# OVHcloud Design System - Complete Documentation

> OVHcloud Design System is a collection of assets, guidelines and UI components for building consistent user experiences across OVHcloud products.

This document contains the complete OVHcloud Design System documentation including all components, styling, theming, guides, and tools.

## Helpers

# formatPrice

## Specifications

---

Format a numeric value to a price matching given locale format with expected currency.

See also:

-   [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat)

| Argument | Type | Default value | Description |
| --- | --- | --- | --- |
| value | `number` | `   undefined   ` | The numeric value to format. |
| locale | `string` | `'fr-FR'` | The locale to use. |
| currency | `string` | `'EUR'` | The currency expected. |

## Examples

---

### Default

123 456,79 €

```jsx
<>{formatPrice(123456.789)}</>
```

### Locales

France: 123 456,79 €

Germany: 123.456,79 €

Arabic: ‏١٢٣٬٤٥٦٫٧٩ €

India: €1,23,456.79

```jsx
<>
  <p>France: {formatPrice(123456.789, 'fr-FR')}</p>
  <p>Germany: {formatPrice(123456.789, 'de-DE')}</p>
  <p>Arabic: {formatPrice(123456.789, 'ar-EG')}</p>
  <p>India: {formatPrice(123456.789, 'en-IN')}</p>
</>
```

### Currencies

Euro: 123 456,79 €

Dollar: 123 456,79 $

Yen: 123 457 ¥

```jsx
<>
  <p>Euro: {formatPrice(123456.789, 'fr-FR', 'EUR')}</p>
  <p>Dollar: {formatPrice(123456.789, 'fr-FR', 'USD')}</p>
  <p>Yen: {formatPrice(123456.789, 'fr-FR', 'JPY')}</p>
</>
```

## Helpers/formatPrice

## Examples


### Currencies

```tsx
{
  globals: {
    imports: `import { formatPrice } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Euro: {formatPrice(123456.789, 'fr-FR', 'EUR')}</p>
      <p>Dollar: {formatPrice(123456.789, 'fr-FR', 'USD')}</p>
      <p>Yen: {formatPrice(123456.789, 'fr-FR', 'JPY')}</p>
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { formatPrice } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>{formatPrice(123456.789)}</>
}
```

### Locales

```tsx
{
  globals: {
    imports: `import { formatPrice } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>France: {formatPrice(123456.789, 'fr-FR')}</p>
      <p>Germany: {formatPrice(123456.789, 'de-DE')}</p>
      <p>Arabic: {formatPrice(123456.789, 'ar-EG')}</p>
      <p>India: {formatPrice(123456.789, 'en-IN')}</p>
    </>
}
```

## Helpers

# formatRelativeTime

## Specifications

---

Format a date to a human readable relative time (like 'a month ago').

See also:

-   [Intl.RelativeTimeFormatOptions](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat)

| Argument | Type | Default value | Description |
| --- | --- | --- | --- |
| date | `Date` | `   undefined   ` | The date to format. |
| locale | `string` | `'en-GB'` | The locale to use. |
| option | `RelativeTimeFormatOptions` | `   undefined   ` | Language-sensitive formatting options. |

## Examples

---

### Default

in 0 seconds

```jsx
<>{formatRelativeTime(new Date())}</>
```

### Locales

France: il y a 0 seconde

Germany: in 0 Sekunden

Arabic: قبل ٠ ثانية

India: in 0 seconds

```jsx
<>
  <p>France: {formatRelativeTime(new Date(), 'fr-FR')}</p>
  <p>Germany: {formatRelativeTime(new Date(), 'de-DE')}</p>
  <p>Arabic: {formatRelativeTime(new Date(), 'ar-EG')}</p>
  <p>India: {formatRelativeTime(new Date(), 'en-IN')}</p>
</>
```

### Format option

Long: in 0 seconds

Short: in 0 sec.

Narrow: in 0s

```jsx
<>
  <p>Long: {formatRelativeTime(new Date(), 'en-US', {
    style: 'long'
  })}</p>
  <p>Short: {formatRelativeTime(new Date(), 'en-US', {
    style: 'short'
  })}</p>
  <p>Narrow: {formatRelativeTime(new Date(), 'en-US', {
    style: 'narrow'
  })}</p>
</>
```

## Helpers/formatRelativeTime

## Examples


### Default

```tsx
{
  globals: {
    imports: `import { formatRelativeTime } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>{formatRelativeTime(new Date())}</>
}
```

### Locales

```tsx
{
  globals: {
    imports: `import { formatRelativeTime } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>France: {formatRelativeTime(new Date(), 'fr-FR')}</p>
      <p>Germany: {formatRelativeTime(new Date(), 'de-DE')}</p>
      <p>Arabic: {formatRelativeTime(new Date(), 'ar-EG')}</p>
      <p>India: {formatRelativeTime(new Date(), 'en-IN')}</p>
    </>
}
```

### Option

```tsx
{
  globals: {
    imports: `import { formatRelativeTime } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Long: {formatRelativeTime(new Date(), 'en-US', {
        style: 'long'
      })}</p>
      <p>Short: {formatRelativeTime(new Date(), 'en-US', {
        style: 'short'
      })}</p>
      <p>Narrow: {formatRelativeTime(new Date(), 'en-US', {
        style: 'narrow'
      })}</p>
    </>
}
```

## Apply ODS Style

---

After installing ODS (following the  documentation), you'll be able to use every components with the correct style.

For the non-component related style, you need to update your app accordingly.

## Use the ODS font

---

ODS components does not enforce any font by default (minus a few exception like `ods-code`).

To use one of the ODS fonts in your app, add the expected variables to the top of your app:

```css
html {
  font-family: var(--ods-font-family-default);
}
```

## Use the ODS design tokens

---

All ODS components are using common design tokens, that are accessible through [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) .

If you want to use any of those on your own components, you can refer to the exhaustive list on the [Figma dedicated page](https://www.figma.com/design/tIKzHa5KvHHyosgIgyBswB/Design-Tokens?m=auto&node-id=0-1&t=s57Qt3pa7WuFWKEh-1) .

```css
.my-custom-title {
  color: var(--ods-color-text);
  ...
}
```

## Use ODS CSS reset

---

We recommend you to import the ODS CSS reset subset of rules to normalize the rendering of some elements, but it's not mandatory.

```typescript
import '@ovhcloud/ods-react/normalize-css';
```

## Design Tokens

---

Design tokens define reusable design values such as colors, borders, and outlines as variables. Instead of using hardcoded values, they provide a consistent, scalable, and maintainable way to style components across all products.

## Usage

---

Design tokens are available as CSS custom properties (variables) and can be used in your CSS files:

```css
.my-component {
  color: var(--ods-color-primary-500);
  border-radius: var(--ods-border-radius-sm);
}
```

## Theme Variables

---

### Color

| Token | Value | Preview |
| --- | --- | --- |
| --ods-theme-anchor-text-color | #0050d7 | 
 |
| --ods-theme-anchor-text-color-hover | #002dbe | 

 |
| --ods-theme-anchor-text-color-visited | #000e9c | 

 |
| --ods-theme-background-color | #fff | 

 |
| --ods-theme-background-color-disabled | #e6e6e6 | 

 |
| --ods-theme-background-color-drag-over | var(--ods-color-primary-100) | 

 |
| --ods-theme-background-color-readonly | #f2f2f2 | 

 |
| --ods-theme-background-color-selected | #00185e | 

 |
| --ods-theme-border-color-disabled | #cccccc | 

 |
| --ods-theme-border-color-drag-over | var(--ods-color-primary-100) | 

 |
| --ods-theme-border-color-readonly | #f2f2f2 | 

 |
| --ods-theme-border-color-selected | #00185e | 

 |
| --ods-theme-brand-color | #000e9c | 

 |
| --ods-theme-heading-text-color | #00185e | 

 |
| --ods-theme-progress-background-color | var(--ods-color-primary-500) | 

 |
| --ods-theme-split-background-color | #e6e6e6 | 

 |
| --ods-theme-split-border-color | #e6e6e6 | 

 |
| --ods-theme-text-color | #4d5592 | 

 |
| --ods-theme-text-color-disabled | #808080 | 

 |
| --ods-theme-text-color-selected | #ffffff | 

 |
| --ods-theme-track-background-color | var(--ods-color-neutral-100) | 

 |
| --ods-theme-track-background-color-disabled | var(--ods-color-neutral-500) | 

 |
| --ods-theme-critical-color | var(--ods-color-critical-500) | 

 |
| --ods-theme-information-color | var(--ods-color-information-500) | 

 |
| --ods-theme-neutral-color | var(--ods-color-neutral-500) | 

 |
| --ods-theme-primary-color | var(--ods-color-primary-500) | 

 |
| --ods-theme-success-color | var(--ods-color-success-500) | 

 |
| --ods-theme-warning-color | var(--ods-color-warning-500) | 

 |
| --ods-theme-chart-background-color | var(--ods-theme-background-color) | 

 |
| --ods-theme-chart-axis-color | var(--ods-color-neutral-300) | 

 |
| --ods-theme-chart-tick-color | var(--ods-color-neutral-700) | 

 |
| --ods-theme-chart-legend-color | var(--ods-color-neutral-700) | 

 |
| --ods-theme-chart-grid-color | var(--ods-color-neutral-200) | 

 |
| --ods-theme-chart-reference-line-color | var(--ods-theme-critical-color) | 

 |

### Outline

| Token | Value | Preview |
| --- | --- | --- |
| --ods-theme-outline-color | #000e9c | 
 |
| --ods-theme-outline-offset | 2px | 

 |
| --ods-theme-outline-style | solid | 

 |
| --ods-theme-outline-width | 2px | 

 |

### Overlay

| Token | Value | Preview |
| --- | --- | --- |
| --ods-theme-backdrop-background-color | #0050d7 | 
 |
| --ods-theme-backdrop-opacity | 0.75 | 

 |
| --ods-theme-overlay-border-radius | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-theme-overlay-box-shadow | 0 2px 8px rgba(0, 14, 156, 0.2) | 

 |
| --ods-theme-overlay-z-index | 99 | 

 |

### Spacing

| Token | Value | Preview |
| --- | --- | --- |
| --ods-theme-border-radius | 8px | 
 |
| --ods-theme-border-width | 1px | 

 |
| --ods-theme-column-gap | 8px | 

 |
| --ods-theme-padding-horizontal | 8px | 

 |
| --ods-theme-padding-vertical | 8px | 

 |
| --ods-theme-row-gap | 8px | 

 |
| --ods-theme-transition-duration | 300ms | 

 |

### Font Family

| Token | Value | Preview |
| --- | --- | --- |
| --ods-theme-font-family | "Source Sans Pro", "Trebuchet MS", arial, "Segoe UI", sans-serif | 
Aa



 |
| --ods-theme-font-family-code | "Source Code Pro", arial | 

Aa



 |

### Form Element

| Token | Value | Preview |
| --- | --- | --- |
| --ods-theme-input-background-color-checked | #0050d7 | 
 |
| --ods-theme-input-background-color-checked-hover | #000e9c | 

 |
| --ods-theme-input-background-color-invalid | #bf0020 | 

 |
| --ods-theme-input-border-color | #b3b3b3 | 

 |
| --ods-theme-input-border-color-checked | #0050d7 | 

 |
| --ods-theme-input-border-color-checked-hover | #000e9c | 

 |
| --ods-theme-input-border-color-hover | #808080 | 

 |
| --ods-theme-input-border-color-invalid | #bf0020 | 

 |
| --ods-theme-input-border-radius | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-theme-input-border-width | var(--ods-theme-border-width) | 

 |
| --ods-theme-input-min-height | 32px | 

 |
| --ods-theme-input-option-background-color-hover | #bef1ff | 

 |
| --ods-theme-input-option-background-color-selected | #e6faff | 

 |
| --ods-theme-input-option-background-color-selected-hover | #bef1ff | 

 |
| --ods-theme-input-padding-horizontal | var(--ods-theme-padding-horizontal) | 

 |
| --ods-theme-input-padding-vertical | calc(var(--ods-theme-padding-vertical) / 4) | 

 |
| --ods-theme-input-placeholder-text-color | #666666 | 

 |
| --ods-theme-input-text-color | var(--ods-theme-text-color) | 

 |
| --ods-theme-input-text-color-checked | var(--ods-theme-background-color) | 

 |
| --ods-theme-input-text-color-invalid | #bf0020 | 

 |

## Color Palettes

---

  

## Previous tokens

Deprecated

---

Following tokens have been deprecated and will be removed in a future major release.  
Non spacing tokens have been renamed with the `--ods-theme` prefix.  
Spacing tokens have been reduced to one base value, you should set your values based on this base, for example:`--my-border-radius-sm: calc(var(--ods-theme-border-radius)) / 2;`.

| Token | Value | Preview |
| --- | --- | --- |
| --ods-border-radius-xs | 2px | 
 |
| --ods-border-radius-sm | 4px | 

 |
| --ods-border-radius-md | 8px | 

 |
| --ods-border-radius-lg | 16px | 

 |
| --ods-border-width-sm | 1px | 

 |
| --ods-border-width-md | 2px | 

 |
| --ods-color-background-disabled-default | #e6e6e6 | 

 |
| --ods-color-background-readonly-default | #f2f2f2 | 

 |
| --ods-color-border-disabled-default | #cccccc | 

 |
| --ods-color-border-readonly-default | #f2f2f2 | 

 |
| --ods-color-element-background-selected | #00185e | 

 |
| --ods-color-element-text-selected | #ffffff | 

 |
| --ods-color-form-element-background-critical | #ffffff | 

 |
| --ods-color-form-element-background-default | #ffffff | 

 |
| --ods-color-form-element-background-focus-default | #ffffff | 

 |
| --ods-color-form-element-background-hover-default | #ffffff | 

 |
| --ods-color-form-element-background-selected-critical | #bf0020 | 

 |
| --ods-color-form-element-border-critical | #bf0020 | 

 |
| --ods-color-form-element-border-default | #b3b3b3 | 

 |
| --ods-color-form-element-border-focus-default | #808080 | 

 |
| --ods-color-form-element-border-hover-default | #808080 | 

 |
| --ods-color-form-element-text-default | #4d5592 | 

 |
| --ods-color-form-element-text-placeholder-default | #666666 | 

 |
| --ods-color-heading | #00185e | 

 |
| --ods-color-text | #4d5592 | 

 |
| --ods-color-text-disabled-default | #808080 | 

 |
| --ods-font-family-code | "Source Code Pro", arial | 

Aa



 |
| --ods-font-family-default | "Source Sans Pro", "Trebuchet MS", arial, "Segoe UI", sans-serif | 

Aa



 |
| --ods-form-element-input-height | 32px | 

 |
| --ods-outline-color-default | #000e9c | 

 |
| --ods-outline-offset | 2px | 

 |
| --ods-outline-style-default | solid | 

 |
| --ods-outline-width | 2px | 

 |

## Style Customization

---

Although ODS components comes with the expected designs, it is sometime useful to customize the rendering of a component to fit your context.

Here we'll describe different ways to customize an ODS component.

## Apply style directly on the component

---

The ODS component host behave like any other React element, so you can apply a class directly to it.

For example, if you want to display a vertical list of `Link`, you can override the inline default display:

```html
<Link className="custom-link">
  My link 1
</Link>
<Link className="custom-link">
  My link 2
</Link>
<style>
  .custom-link {
    display: block;
  }
</style>
```

As there is no shadow DOM anymore, all of the component DOM can be targeted through CSS selectors. It is advised to not rely on cascading selector, as the DOM structure may evolve from one version to another.

  

To ensure reliable CSS, use a specific className on the component you want to customize.

  

If you want to specifically target underneath elements, you can rely on some [data attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/Use_data_attributes#css_access) , we'll update the documentation about those soon.

## Reuse existing ODS style

---

In some cases where you cannot use an ODS component, you may want one of your own components to look like an ODS component (for example, when using an external library that doesn't allow custom templating).

We did expose a bunch of [Sass](https://sass-lang.com) mixins that allows you to easily apply the ODS style to your own elements.

For example, if you need to make an anchor tag looks like a `Link`:

```css
@import '@ovhcloud/ods-react/style';
.my-link {
  @include ods-link();
  @include ods-link-color('primary');
}
```

```html
<a class="my-link"></a>
```

## CSS variables

---

All ODS components are using common design tokens, that are accessible through [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) .

We do advise to reuse those variables on your own style, this way your application will automatically be updated on current theme changes or when new themes will be released.

You can find the whole list on the  documentation page.

Example of CSS variables:

```html
<h1 class="my-own-page-title">
  Welcome page
</h1>
<style>
  .my-own-page-title {
    color: var(--ods-color-primary-500);
  }
</style>
```

## Tailwind CSS Integration

---

This guide covers how to integrate Tailwind CSS with the OVHcloud Design System (ODS) for both v3 and v4 versions.

Tailwind CSS is a utility-first CSS framework that can be integrated with ODS components and design tokens, but it is not mandatory as ODS is not based on Tailwind.

Technically, you don't need any configuration to make ODS work with Tailwind. These guides are here to add some auto-completion and best practices when using Tailwind, but you can directly use the design tokens right out of the box without any additional configuration.

## Tailwind CSS v3 Configuration

---

For Tailwind CSS v3, create a `tailwind.config.js` which contains a remapping of ODS variables:

```js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      borderRadius: {
        'ods-border-radius-xs': 'var(--ods-border-radius-xs)',
        'ods-border-radius-sm': 'var(--ods-border-radius-sm)',
        'ods-border-radius-md': 'var(--ods-border-radius-md)',
        'ods-border-radius-lg': 'var(--ods-border-radius-lg)',
      },
      colors: {
        'ods-color-primary-500': 'var(--ods-color-primary-500)',
        'ods-color-success-500': 'var(--ods-color-success-500)',
        'ods-color-warning-700': 'var(--ods-color-warning-700)',
      },
      fontFamily: {
        'ods-font-family-default': 'var(--ods-font-family-default)',
      },
    },
  },
  plugins: [],
}
```

### Integration with IDEs

#### Integration with JetBrains

This should work straight out of the box.

#### Integration with VSCode

Create a `./.vscode/tailwind.json` containing the classes you want to expose for autocompletion.

```json
{
    "customClasses": [
      "ods-color-primary-500",
      "ods-color-success-500",
      "ods-color-critical-700",
      "ods-border-radius-xs",
      "ods-border-radius-sm",
      "ods-border-radius-md",
      "ods-border-radius-lg",
      "ods-font-family-default",
    ]
  }
```

## Tailwind CSS v4 Configuration

---

For Tailwind CSS v4, create a CSS file using the [@theme](https://tailwindcss.com/docs/adding-custom-styles) property:

```css
@theme {
  --radius-ods-xs: var(--ods-border-radius-xs);
  --radius-ods-sm: var(--ods-border-radius-sm);
  --radius-ods-md: var(--ods-border-radius-md);
  --radius-ods-lg: var(--ods-border-radius-lg);
  --color-ods-primary: var(--ods-color-primary-500);
  --color-ods-success: var(--ods-color-success-500);
  --color-ods-critical: var(--ods-color-critical-700);
  --font-ods-default: var(--ods-font-family-default);
}
```

Then import that file after the main ODS theme and Tailwind CSS. In order for you to properly be able to overwrite the ODS theme, you need to redefine the order of the CSS layers. Base is optional.

```typescript
// index.css
@layer theme, base, components, ods-quarks, ods-atoms, ods-molecules, ods-organisms, molecules, utilities;
@import "tailwindcss";
@import '@ovhcloud/ods-themes/default/css';
@import '@ovhcloud/ods-themes/default/fonts';
@import './ods-tailwind.css';
```

## Usage Examples

---

You can use Tailwind utilities alongside ODS components:

```tsx
import { Button } from '@ovhcloud/ods-react';
function MyComponent() {
  return (
  <div>
      <p className="text-critical">Click on the button!</p>
      <Button className="bg-color-primary">Click me!</Button>
  </div>
  );
}
```

## Best Practices & Troubleshooting

---

### Color Usage

-   Use semantic color names (e.g., `ods-primary-500`, `ods-success-200`) instead of generic colors
-   **Issue**: Color classes not working? Verify that the Tailwind config includes the ODS color mappings and ensure the content paths include your component files.

### Spacing and Layout

-   Use ODS border radius tokens (`rounded-ods-sm`, `rounded-ods-md`, etc.)
-   Leverage ODS form element heights for consistent input sizing
-   **Issue**: CSS variables not loading? Ensure `@ovhcloud/ods-themes` is imported before Tailwind and check that the theme is properly initialized in your app.

### Typography

-   Use the ODS font family (`font-ods` for body text, `font-ods-code` for code)
-   Apply semantic text colors (`text-ods-text`, `text-ods-heading`)
-   Maintain proper contrast ratios with ODS color combinations

### Component Integration

-   ODS components can be styled with Tailwind utilities using the `className` prop
-   Avoid overriding core ODS component styles unless necessary
-   Use Tailwind for layout and positioning, ODS for component behavior
-   **Issue**: Responsive breakpoints not working? Confirm that the ODS breakpoint variables are properly defined and check that the screen configuration in Tailwind config is correct.

## Resources

---

-   [Tailwind CSS Documentation](https://tailwindcss.com/docs)
-   [Tailwind v4 Migration Guide](https://tailwindcss.com/docs/upgrade-guide)
-   
-

## Frequently Asked Questions

---

_Welcome to the F.A.Q. section of the OVHcloud Design System (ODS)._

_Here, we aim to address common questions and provide helpful guidance for developers._

## How can I contribute?

---

OVHcloud Design System is an evolving project, and we welcome contributions from the community. By keeping our repository open source, we aim to make it easier for anyone to suggest improvements, fix issues, or share ideas.

Whether it’s a small fix or a new feature, every contribution helps improve the quality and usability of the system. We appreciate the time and effort of anyone who chooses to take part.

Please note that while we welcome contributions, we encourage you to discuss major changes with our team to ensure alignment with the project's goals and overall quality. Additionally, if you're interested in proposing a new feature, please reach us out first to avoid duplicating effort, as we may already be working on a similar development.

Refer to our project [README](https://github.com/ovh/design-system?tab=contributing-ov-file#contributing-to-ods-project) to learn more about installation and pre-requisites.

## How are releases managed?

---

The OVHcloud Design System follows a structured release process to deliver improvements regularly while letting teams adopt updates at their own pace.

**Patch releases** deliver critical fixes or small improvements and can be adopted immediately.

**Minor releases** introduce new non-breaking features and start the deprecation process when needed.

**Major releases** contain breaking changes, removals of previously deprecated elements, and migration documentation.

Releases are published several times a year based on user needs and product evolution.

We support two major versions at the same time:

-   The current major version receives full support (features, improvements, fixes).
-   The previous major version receives maintenance support for six months (critical fixes only).
-   Older versions are no longer supported.

Deprecations follow a two-step workflow:

-   They are announced in a minor release, marked as "Deprecated" in Storybook, and paired with migration guidance.
-   They are removed in the next major release.

Teams may upgrade at their own pace and can skip versions. We recommend pinning a specific ODS version in your project to ensure consistent behavior.

## I can't find a component I need

---

ODS provides base components that you can build on top of and implement in various contexts.

If you need a new component, we recommend working with the design team to request and design the component to ensure it aligns with the ODS guidelines.

## Is my browser supported?

---

ODS supports the latest 2 versions of the following browsers:

-   Google Chrome
-   Microsoft Edge
-   Mozilla Firefox
-   Opera
-   Safari

## Can I use another CSS framework on top of ODS?

---

You can use any CSS framework on top of ODS.

Note that some imperative CSS frameworks like Bootstrap may override ODS styling.

Ensure compatibility by testing the integration in your project.

## What about Web Components and Vue?

---

Starting from v19, ODS have moved to fully React-based components. Web Components and the Vue wrapper are now only available in v18.x, which is now in maintenance mode, so there won't be any new features for this version.

If you still need to access documentation about Web Components or Vue, use the version selector at the top left to switch to the latest 18.x documentation, where you'll find the most up-to-date information.

## TS2307: Cannot find module '@ovhcloud/ods-react' or its corresponding type declarations.

---

Check that you're using the `bundler` [module resolution](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#--moduleresolution-bundler) in your `tsconfig.json`.

```typescript
"moduleResolution": "bundler"
```

## My popover/tooltip is not displayed correctly

---

Check that you're not using a `Fragment` as your trigger.

The following will **not** work properly:

```tsx
<Tooltip>
  <TooltipTrigger asChild>
    <>
      <Component1 />
      <Component2 />
    </>
  </TooltipTrigger>
  <TooltipContent>
    ...  </TooltipContent>
</Tooltip>
```

Replace the `Fragment` with an actual node:

```tsx
<Tooltip>
  <TooltipTrigger asChild>
    <MyTrigger>
      ...    </MyTrigger>
  </TooltipTrigger>
  <TooltipContent>
    ...  </TooltipContent>
</Tooltip>
```

## Overlay elements are not displayed correctly in a drawer/modal

---

`Modal` and `Drawer` components have a `z-index` greater than other overlay components. This is to prevent those to appear behind an open modal/drawer.

To allow overlay elements to correcty render inside a modal/drawer, add the `createPortal={ false }` attribute to the `Content` component of the overlay element.

See  and .

## Icons are not displayed

---

Ensure you did import the fonts in your application, check the  page to read more about how to achieve this.

## Warning: Function components cannot be given refs

---

When using components enabling dynamic component (through the `as` attribute), you may encounter the following error message in the console:

```
Warning: Function components cannot be given refs.
Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
```

This means you're passing a function component to the `as` attribute and it needs to be wrapped by a `forwardRef` to be able to manage the `ref` correctly.

```tsx
import { Link } from '@ovhcloud/ods-react';
import { forwardRef, useRef } from 'react';
const DummyLink = forwardRef(({ children, ...props }, ref) => {
  return (
    <a data-test="dummy" { ...props } ref={ ref }>{ children }</a>
  );
});
const MyApp = () => {
  const linkRef = useRef(null);
  return (
    <div>
      ...      <Link as={ DummyLink } href="#" ref={ linkRef }>...</Link>
    </div>
  );
};
```

## Get Started

---

OVHcloud Design System is a set of reusable React UI components to create a consistent user experience across OVHcloud pages and products, acting as a single source of truth.

All OVHcloud Design System packages are available on [NPM](https://www.npmjs.com) .

## Installation

---

Include **ODS** in your projects like any other npm/yarn module:

```bash
npm i --save-exact @ovhcloud/ods-react @ovhcloud/ods-themes
```

or

```bash
yarn add --exact @ovhcloud/ods-react @ovhcloud/ods-themes
```

## Usage

---

All components can be imported from the root path:

```tsx
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ovhcloud/ods-react';
const App = () => {
  return (
    <Accordion>
      <AccordionItem value="0">
        <AccordionTrigger>
          Hello World!        </AccordionTrigger>
        <AccordionContent>
          Lorem ipsum dolor sit amet.        </AccordionContent>
      </AccordionItem>
    </Accordion>
  );
};
```

Each component is also available as compound component from its subpath:

```tsx
import { Accordion } from '@ovhcloud/ods-react/accordion';
const App = () => {
  return (
    <Accordion>
      <Accordion.Item value="0">
        <Accordion.Trigger>
          Hello World!        </Accordion.Trigger>
        <Accordion.Content>
          Lorem ipsum dolor sit amet.        </Accordion.Content>
      </Accordion.Item>
    </Accordion>
  );
};
```

Components are compatible with React `v18.2+`, `v19+`.

## Import components constants

---

All enums, interfaces, events are available directly from the `ods-react` lib

```typescript
import { BUTTON_SIZE, type CheckboxProp } from '@ovhcloud/ods-react';
```

## Import theme & fonts

---

You need to import a theme to display all the components correctly. It defines all the colors, typographies, etc...

via ES import:

```typescript
import '@ovhcloud/ods-themes/default/css';
import '@ovhcloud/ods-themes/default/fonts';
```

via Sass import:

```css
@import '@ovhcloud/ods-themes/default/css';
@import '@ovhcloud/ods-themes/default/fonts';
```

In case you encounter some issues while importing the font files, you can fallback to the base64 import using `import '@ovhcloud/ods-themes/default`. Though this will increase the bundle size and prevent browser resources loading optimization.

## Typescript

---

To ensure the types are correctly resolved, you'll have to use the `bundler` module resolution, released with [Typescript 5.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#--moduleresolution-bundler) .

```typescript
"moduleResolution": "bundler"
```

## Accessibility Guide

---

## Introduction

---

This guide focuses on accessibility best practices specifically for developers integrating the **OVHcloud Design System**.

While our components are built with accessibility in mind, using semantic HTML elements and including the necessary ARIA attributes by default. Nevertheless, ensuring full accessibility in your specific use case might require you to implement additional ARIA attributes, roles, and behaviors at the integration level.

However, depending on the context, you might need to enhance a component's accessibility. This guide will help you understand:

-   When and how to use ARIA properly
-   Common mistakes and best practices
-   How screen readers interpret different ARIA attributes

For broader accessibility information beyond the **OVHcloud Design System**, consult our [Accessibility Statement](https://www.ovhcloud.com/fr/accessibility/) available on our official website.

### Before using ARIA

#### Consult the Accessibility section for each component

Each component in **OVHcloud Design System** comes with a dedicated **"Accessibility"** section that provides best practices and usage guidance.

#### Be careful when adding extra ARIA attributes as ODS components may already handle it

To be as close as possible to the accessible experiences provided by modern browsers, **OVHcloud Design System** aims to rely on semantic and native HTML elements whenever they are inherently well-suited for screen reader experiences.

In addition to the predefined accessibility features of our components, you can find more guidance regarding accessible integration in the **"Accessibility"** section.

## Essential ARIA attributes

---

### aria-label: Providing an accessible name

Use [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) when an element needs a descriptive name but has no visible text.

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Button, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Button>
      <Icon name={ICON_NAME.xmark} />
    </Button>
}
```

Screen readers will announce a button.

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Button, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Button aria-label='Clear'>
        <Icon name={ICON_NAME.xmark} />
    </Button>
}
```

Screen readers will announce a button and the aria-label.

#### Related Sources

-   [MDN Documentation - aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label)
-   [RGAA - 6.1](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#6.1)
-   [RGAA - 7.2](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#7.2)
-   [RGAA - 11.1](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.1)
-   [RGAA - 11.2](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.2)
-   [RGAA - 11.9](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.9)

### aria-labelledby: Referencing another element as a label

Use [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby) to associate an element with an existing label (instead of duplicating text).

Filter your search results

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Button, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button>
        <Icon name={ICON_NAME.filter} />
      </Button>
      <span>Filter your search results</span>
    </>
}
```

Screen readers will announce a button.

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Button, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button aria-labelledby="filter-btn">
        <Icon name={ICON_NAME.filter} />
      </Button>
      <span id="filter-btn">Filter your search results</span>
    </>
}
```

When the **Button** is focused, the screen reader should announce the linked label.

Use [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) when there is no visible label, and [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby) when there is one.

#### Related Sources

-   [MDN Documentation - aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby)
-   [RGAA - 6.1](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#6.1)
-   [RGAA - 7.2](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#7.2)
-   [RGAA - 11.1](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.1)
-   [RGAA - 11.2](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.2)
-   [RGAA - 11.5](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.5)
-   [RGAA - 11.9](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.9)

### aria-describedby: Providing additional context

[aria-describedby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-describedby) associates an interactive element with extra information, ensuring screen reader users receive helpful guidance without disrupting the UI.

#### Difference between aria-labelledby and aria-describedby

| Attribute | Purpose | When to use it? | Reading order |
| --- | --- | --- | --- |
| aria-labelledby | Defines the main context of the element | When an element has no visible text or needs an external label | Read first by the screen reader |
| aria-describedby | Provides additional description | When secondary information is needed to clarify context | Read after the main label |

#### When to use aria-labelledby vs aria-describedby?

| Use case | `aria-labelledby` | `aria-describedby` |
| --- | --- | --- |
| Use an external / hidden label |  |  |
| Add complementary information |  |  |
| Associate multiple elements to form a label (field group) |  |  |
| Provide contextual help / information (e.g., input rules, field error) |  |  |

```jsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal        </Button>
      </ModalTrigger>
      <ModalContent>
        <ModalHeader><span id="modal-title">Delete item</span></ModalHeader>
        <ModalBody>
          <p id="modal-content">
            Are you sure you want to delete this item? This action cannot be undone.          </p>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

Screen readers won't announce the **Modal** content.

```jsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal        </Button>
      </ModalTrigger>
      <ModalContent aria-describedby="modal-content" aria-labelledby="modal-title">
        <ModalHeader><span id="modal-title">Delete item</span></ModalHeader>
        <ModalBody>
          <p id="modal-content">
            Are you sure you want to delete this item? This action cannot be undone.          </p>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

Screen readers will announce the **Modal** content.

#### Related Sources

-   [MDN Documentation - aria-describedby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-describedby)
-   [RGAA - 11.5](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.5)
-   [RGAA - 11.10](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.10)

### aria-hidden: Hiding elements from screen readers

Use [aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden) set to `"true"` on elements that are purely decorative and should not be announced by screen readers.

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon name={ICON_NAME.home} />
}
```

Some screen readers will announce the icon and detect the presence of the image.

Depending on the screen reader, some additional information might be read aloud, which is confusing and unhelpful.

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon name={ICON_NAME.home} />
}
```

Screen readers will announce nothing, the icon is ignored.

#### Related Sources

-   [MDN Documentation - aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden)
-   [RGAA - 11.2](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.2)

### aria-busy: Indicating loading state

When a section of the UI is loading, users should be informed that the content is not yet ready.

[aria-busy](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-busy) set to `"true"` signals that an element is currently in a loading state. Once loading is complete, `aria-busy` should be set to `"false"`.

```jsx
{
  globals: {
    imports: 'import { Skeleton } from \'@ovhcloud/ods-react\';'
  },
  tags: ['!dev'],
  render: ({}) => <div>
      <Skeleton />
    </div>
}
```

Screen readers will give no indication that the content is loading. The screen reader may start reading the page without knowing that this section is waiting to be updated.

```jsx
{
  globals: {
    imports: 'import { Skeleton } from \'@ovhcloud/ods-react\';'
  },
  tags: ['!dev'],
  render: ({}) => <div aria-busy="true">
      <Skeleton />
    </div>
}
```

Screen readers understand that the content is not yet ready, so they delay announcing or interacting with it.

#### Related Sources

-   [MDN Documentation - aria-busy](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-busy)
-   [RGAA - 7.4](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#7.4)

### status role: Announcing dynamic status changes

Use [status role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/status_role) to ensure that important updates are announced by screen readers without disrupting focus.

```jsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: () => {
    const [message, setMessage] = useState('');
    const handleClick = () => {
      setMessage('Copied to clipboard.');
    };
    return <>
        <Button onClick={handleClick}>
          Copy        </Button>
        <span style={{
        marginLeft: '1rem'
      }}>
          {message}
        </span>
      </>;
  }
}
```

Screen readers will announce the button and its label.

```jsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: () => {
    const [message, setMessage] = useState('');
    const handleClick = () => {
      setMessage('Copied to clipboard.');
    };
    return <>
        <Button onClick={handleClick}>
          Copy        </Button>
        <span aria-live="polite" role="status" style={{
        marginLeft: '1rem'
      }}>
          {message}
        </span>
      </>;
  }
}
```

Screen readers will announce the button and its label, and the message on click or press.

It also matters since it keeps focus on the button, avoiding disruption for keyboard users.

#### Related Sources

-   [MDN Documentation - status role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/status_role)
-   [RGAA - 7.4](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#7.4)

### alert role: Announcing important messages

Use [alert role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/alert_role) for critical, time-sensitive messages that require immediate attention. Unlike `status role`, `alert role` is announced by screen readers, even if the user is focused elsewhere.

```jsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: () => {
    const [error, setError] = useState('');
    const handleClick = () => {
      setError('A critical error occurred while saving.');
    };
    return <>
        <Button onClick={handleClick}>
          Save        </Button>
        <span style={{
        marginLeft: '1rem',
        color: 'red'
      }}>
          {error}
        </span>
      </>;
  }
}
```

Screen readers won’t immediately announce the error message when it appears, unless the user manually navigates to it.

```jsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: () => {
    const [error, setError] = useState('');
    const handleClick = () => {
      setError('A critical error occurred while saving!');
    };
    return <>
        <Button onClick={handleClick}>
          Save        </Button>
        <span role="alert" style={{
        marginLeft: '1rem',
        color: 'red'
      }}>
          {error}
        </span>
      </>;
  }
}
```

Screen readers will announce the error message, even when the user is focused elsewhere.

#### Related Sources

-   [MDN Documentation - alert role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/alert_role)
-   [RGAA - 7.4](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#7.4)

### radiogroup and radio roles: Making custom radio controls accessible

[radiogroup](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/radiogroup_role) and [radio](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/radio_role) roles ensure that screen readers correctly interpret and announce the selection.

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <label htmlFor="rating">
        Rating      </label>
      <div id="rating">
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="one star" role="img" />
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="two star" role="img" />
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="three star" role="img" />
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="four star" role="img" />
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="five star" role="img" />
      </div>
    </>
}
```

Screen readers will read each star individually with no context of selection group.

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <label htmlFor="rating" id="rating-label">
        Rating      </label>
      <div aria-labelledby="rating-label" id="rating" role="radiogroup">
        <Icon name={ICON_NAME.star} role="radio" tabIndex={-1} aria-label="one star" aria-checked="false" />
        <Icon name={ICON_NAME.star} role="radio" tabIndex={0} aria-label="two star" aria-checked="true" />
        <Icon name={ICON_NAME.star} role="radio" tabIndex={-1} aria-label="three star" aria-checked="false" />
        <Icon name={ICON_NAME.star} role="radio" tabIndex={-1} aria-label="four star" aria-checked="false" />
        <Icon name={ICON_NAME.star} role="radio" tabIndex={-1} aria-label="five star" aria-checked="false" />
      </div>
    </>
}
```

Screen readers will announce a radio button, its label and aria-label, which item is focused on out of the total amount of items and that it is a radio group.

Please note that you should navigate within a radio group using Arrow keys.

In the example above, `tabindex="-1"` is applied to all elements except the currently selected one. This is because only one item in a radio group should be focusable at a time. Users can navigate between options using the Arrow keys, rather than the Tab key, which moves focus to the next interactive element outside the group.

For more details, see [Managing focus within components using a roving tabindex](https://www.w3.org/WAI/ARIA/apg/patterns/radio/examples/radio/)

#### Sources

-   [MDN Documentation - radiogroup](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/radiogroup_role)
-   [MDN Documentation - radio](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/radio_role)
-   [RGAA - 11.5](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/#11.5)

## Final Accessibility check for ARIA roles

---

### Validate the screen reader behavior

Test with a screen reader to ensure that your changes are communicated correctly.

Ensure that actions such as button clicks, form submissions, and changes in state are clearly communicated to the user.

### Testing Environment

For consistent testing across different platforms and screen readers, here is the testing setup used by **OVHcloud Design System**, based on the recommendations from RGAA (France):

| Assistive Technology | Browser |
| --- | --- |
| NVDA | Firefox |
| JAWS | Firefox or Internet Explorer |
| Safari | VoiceOver |

## Resources to help you test accessibility

---

-   [Accessibility France Guidelines (FR)](https://accessibilite.numerique.gouv.fr) : The French government's official accessibility resource for best practices and testing criteria
-   [Web Content Accessibility Guidelines (WCAG) 2.1](https://www.w3.org/TR/WCAG21) : The internationally recognized standards for web accessibility, providing guidelines for creating more accessible web content
-   [WebAIM: Introduction to Web Accessibility](https://webaim.org/intro) : A guide to the principles of web accessibility from WebAIM
-   [WebAIM: Semantic Structure](https://webaim.org/techniques/semanticstructure) : Learn about the importance of semantic HTML and how it impacts accessibility
-   [WebAIM: Contrast Checker](https://webaim.org/resources/contrastchecker) : Tool to ensure sufficient color contrast between text and background for readability
-   [French Accessibility Testing Methodology (FR)](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests) : An in-depth guide to testing web accessibility in France
-   [French Testing Environment (FR)](https://accessibilite.numerique.gouv.fr/methode/environnement-de-test) : Official guidelines on the tools and technologies to use when testing accessibility on desktop computers
-   [W3C Tutorial on Alternative Text for Images](https://www.w3.org/WAI/tutorials/images/decision-tree) : A decision tree for writing appropriate alternative text for images

## Charts - Usage Guidelines

---

These guidelines provide UI, functional, and UX rules to follow when creating charts.

While this documentation is not exhaustive and does not cover technical implementation details, it serves as a comprehensive reference for designing effective and consistent data visualizations.

## General guidelines

---

-   **Consistency**: Maintain consistency in your chart types and styles across your application or platform
-   **Labels**: Clearly label your axes, legend, and data points. Use descriptive titles (or Tooltips) to help users understand the chart's purpose and details
-   **Accessibility**: Ensure your charts are accessible to all users (see [Accessible Charts](https://www.a11y-collective.com/blog/accessible-charts/) )

## Chart types

---

The following chart types are commonly used in OVHcloud, categorized by their primary use cases:

### Bar Charts

**Usage**

-   Comparing quantities or amounts between different categories
-   Displaying data in a stacked layout to show cumulative values

**Best Practices**

-   Prefer using vertical bars rather than horizontal layout
-   Use solid bar colors to distinguish them from background

### Line Charts

**Usage**

-   Showing progression and trends of data over time
-   Comparing multiple lines through a common criterion such as a timespan

**Best Practices**

-   Prefer using solid lines for data series

### Area Charts

**Usage**

-   Presenting the amplitude of content and focusing on drastic trend changes
-   Displaying data in a stacked layout to show cumulative values

**Best Practices**

-   Top of areas must be a solid line to draw area limit
-   Opacity can be applied to areas to show data overlaps

### Pie Charts

**Usage**

-   Displaying proportions and percentages of a whole amount

**Best Practices**

-   Avoid more than 5 slices to keep clarity

## Styling

---

### Structural elements

For structural elements, here are the expected values to apply:

| Element | Token | Color | Preview |
| --- | --- | --- | --- |
| Background | `--ods-theme-chart-background-color` | `#ffffff` | 
 |
| Axis | `--ods-theme-chart-axis-color` | `#b3b3b3` | 

 |
| Ticks (axis labels) | `--ods-theme-chart-tick-color` | `#4d4d4d` | 

 |
| Legends | `--ods-theme-chart-legend-color` | `#4d4d4d` | 

 |
| Grids | `--ods-theme-chart-grid-color` | `#cccccc` | 

 |
| Reference Line | `--ods-theme-chart-reference-line-color` | `#bf0020` | 

 |

-   Font-family must follow ODS pre-set for all text labelling
-   Font-size minimum values to use are:
    -   **12px** for axis labels
    -   **14px** for legends
    -   **16px** for tooltip content
-   ODS Tooltips must be used to define series data while hovering on values

## Color scheme

---

Use the following colors for your chart series, in the specified order:

01#0050d7

02#ac246f

03#00875a

04#df3400

05#25327c

06#a61e20

07#006d77

08#6a2ecf

09#374151

10#4f8400

These colors are picked for their likelihood to be valid regarding color contrast ratio, and can be distinguished from one another.

## Usage Guidelines

---

These guidelines define how forms should behave and be structured across products using the OVHcloud Design System.

This documentation does not describe a technical implementation, but provides functional and UX rules to follow when designing and developing forms.

## Mandatory and optional fields

---

### How to define mandatory fields

A mandatory field must:

-   Explicitly include the word "Mandatory" in the label.
-   Never rely on an asterisk alone.
-   Clearly communicate the expected format in the helper text when relevant.

### Optional fields

Optional fields do not need to be labeled "Optional" and can remain unmarked if mandatory fields are clearly identified.

First name- mandatory

Last name- mandatory

Company

Phone

Include country code (e.g. +33 6 00 00 00 00)

Email- mandatory

Format: name@example.com

## Submit behavior

---

In the OVHcloud Design System, the submit should button remain enabled at all times.

Validation is triggered when the user attempts to submit the form.

The form must provide feedback after user action rather than prevent the action.

### Standard submit flow

Initial state:

-   The submit button is visible and enabled.
-   Mandatory fields are clearly marked in the label.
-   Helper text communicates expected formats when relevant.
-   No error messages are displayed before interaction.

When the user clicks the submit button:

-   All mandatory fields are validated.
-   All format validations are executed.
-   Field-level errors are displayed where needed.
-   A global error may be displayed if applicable.
-   No field values are cleared.

If errors are found:

-   Each invalid field displays its error message.
-   Error styling is applied.
-   Fields remain populated.
-   The submit button remains enabled.
-   The user can immediately correct the fields and resubmit.

If the form is displayed in a **Modal** or **Drawer**:

-   The container remains open.
-   Errors are displayed within the same context.

If no errors are found:

-   The form proceeds to submission.
-   Success feedback is triggered (**Toast** or confirmation page depending on the flow).

### Relationship with inline validation

Inline validation (on blur):

-   May display format errors as soon as the user leaves a field.
-   Improves correction speed.
-   Does not replace submit validation.

On submit:

-   All validations are executed again to ensure consistency.
-   Required errors appear even if the field was never focused.

### Loading & action buttons states

When the user submits a form:

-   The primary action button switches to a loading state.
-   The secondary action, if any, becomes disabled.

This prevents duplicate submissions and conflicting actions, and clearly communicates that the request is being processed.

## Error handling

---

Error handling must clearly communicate what went wrong and how the user can correct it.

Error handling exists at two levels.

Global errors explain why submission failed. Field-level errors explain what needs correction.

### Local errors (field-level)

Field-level errors are tied to a specific input.

They indicate that the value entered in a particular field is invalid or missing.

#### When field-level errors are triggered

Field-level errors can be triggered:

-   On blur (inline validation).
-   On submit.
-   After asynchronous validation (e.g. backend check).

Submit validation must always re-validate all fields, even if inline validation is enabled.

#### Examples of field-level errors

-   Required field left empty.
-   Invalid email format.
-   Password does not meet requirements.

### Global errors (form-level)

Global errors are not tied to a single field.

They occur when:

-   Backend validation fails.
-   A network error occurs.
-   A business rule prevents submission.
-   The action cannot be completed.

#### Types of Global Errors

##### Recoverable Failure

Use a **Toast** when:

-   The failure is temporary.
-   The user can retry immediately.
-   The error does not block form structure.

Example:

-   Temporary network issue.

##### Critical / Blocking Failure

Use a critical Message component when:

-   The submission fails definitively.
-   The user must take action.
-   The error is structural or business-related.
-   The error message is long or detailed.

##### In Modal or Drawer contexts

-   Always use a critical **Message**.
-   The message is displayed inside it.
-   The container remains open.

#### Placement of global error

When using a critical **Message**:

-   It should be placed above the action buttons.
-   It must not replace field-level errors.

### Example

First name- mandatory

Email- mandatory

## Success handling

---

### Direct feedback

Use a **Toast** when:

-   The action is completed successfully.
-   The user stays on the same page.
-   No additional flow is required.

### Confirmation page / modal

Use when:

-   The flow continues.
-   The user moves to a next step.
-   A confirmation summary is required.

## Form structures

---

### Simple form with standard action buttons

Phone number

AfghanistanÅland IslandsAlbaniaAlgeriaAmerican SamoaAndorraAngolaAnguillaAntigua & BarbudaArgentinaArmeniaArubaAustraliaAustriaAzerbaijanBahamasBahrainBangladeshBarbadosBelarusBelgiumBelizeBeninBermudaBhutanBoliviaBosnia & HerzegovinaBotswanaBrazilBritish Indian Ocean TerritoryBritish Virgin IslandsBruneiBulgariaBurkina FasoBurundiCambodiaCameroonCanadaCape VerdeCaribbean NetherlandsCayman IslandsCentral African RepublicChadChileChinaChristmas IslandCocos (Keeling) IslandsColombiaComorosCongo - BrazzavilleCongo - KinshasaCook IslandsCosta RicaCôte d’IvoireCroatiaCubaCuraçaoCyprusCzechiaDenmarkDjiboutiDominicaDominican RepublicEcuadorEgyptEl SalvadorEquatorial GuineaEritreaEstoniaEswatiniEthiopiaFalkland Islands (Islas Malvinas)Faroe IslandsFijiFinlandFranceFrench GuianaFrench PolynesiaGabonGambiaGeorgiaGermanyGhanaGibraltarGreeceGreenlandGrenadaGuadeloupeGuamGuatemalaGuernseyGuineaGuinea-BissauGuyanaHaitiHondurasHong KongHungaryIcelandIndiaIndonesiaIranIraqIrelandIsle of ManIsraelItalyJamaicaJapanJerseyJordanKazakhstanKenyaKiribatiKuwaitKyrgyzstanLaosLatviaLebanonLesothoLiberiaLibyaLiechtensteinLithuaniaLuxembourgMacaoMadagascarMalawiMalaysiaMaldivesMaliMaltaMarshall IslandsMartiniqueMauritaniaMauritiusMayotteMexicoMicronesiaMoldovaMonacoMongoliaMontenegroMontserratMoroccoMozambiqueMyanmar (Burma)NamibiaNauruNepalNetherlandsNew CaledoniaNew ZealandNicaraguaNigerNigeriaNiueNorfolk IslandNorth KoreaNorth MacedoniaNorthern Mariana IslandsNorwayOmanPakistanPalauPalestinePanamaPapua New GuineaParaguayPeruPhilippinesPolandPortugalPuerto RicoQatarRéunionRomaniaRussiaRwandaSamoaSan MarinoSão Tomé & PríncipeSaudi ArabiaSenegalSerbiaSeychellesSierra LeoneSingaporeSint MaartenSlovakiaSloveniaSolomon IslandsSomaliaSouth AfricaSouth KoreaSouth SudanSpainSri LankaSt. BarthélemySt. HelenaSt. Kitts & NevisSt. LuciaSt. MartinSt. Pierre & MiquelonSt. Vincent & GrenadinesSudanSurinameSvalbard & Jan MayenSwedenSwitzerlandSyriaTaiwanTajikistanTanzaniaThailandTimor-LesteTogoTokelauTongaTrinidad & TobagoTunisiaTürkiyeTurkmenistanTurks & Caicos IslandsTuvaluU.S. Virgin IslandsUgandaUkraineUnited Arab EmiratesUnited KingdomUnited StatesUruguayUzbekistanVanuatuVatican CityVenezuelaVietnamWallis & FutunaWestern SaharaYemenZambiaZimbabwe

### Grouped fields form

### Personal Information

First name- mandatory

Last name- mandatory

### Company Information

Company name

VAT number

### Critical form

Please type DELETE to confirm- mandatory

This action is irreversible.

## Accessibility requirements

---

-   "Mandatory" must be visible in the label.
-   Expected input format must be visible in helper text.
-   Error messages must be programmatically associated with fields.
-   Fields must remain populated after validation errors.
-   Inline validation must not trap focus.

## Component selection guidelines

---

This non-exhaustive section helps teams choose the right input component depending on intent. It is presented from the user intention perspective.

Choosing a component is not only about the type of data. It is about:

-   How users think about the choice.
-   Whether comparison is important.
-   Whether precision is required.
-   Whether the action is reversible.
-   The cognitive load of the interface.

The right component reduces friction before validation is even needed.

### When the user needs to provide short, structured information

Use  component.

Use for:

-   Personal information (First name, Last name).
-   Identifiers (Reference number, Customer ID).
-   Short labels (Project name, Tag name).

Why?

-   Users expect a single-line input for short, structured answers.
-   Keeps vertical space minimal.
-   Encourages concise input.

Do not use when:

-   The user must provide explanation or justification.
-   The expected answer may exceed one short sentence.

### When the user needs to explain, justify, or describe something

Use  component.

Use for:

-   Explain why the user is requesting a change.
-   Describe an issue the user encountered.
-   Additional comments.

Why?

-   Visually communicates that longer input is expected.
-   Encourages detailed answers.
-   Prevents frustration caused by limited visible space.

### When the user must create or confirm secure credentials

Use  component.

Use for:

-   Account creation.
-   Password reset.
-   Sensitive authentication fields.

Why?

-   Hides sensitive information.
-   May include strength indicators.
-   Can support show/hide functionality.

### When the user must enter an exact numeric value

Use  or  component

Use for:

-   Quantity of items.
-   Budget amount (precise value).
-   Number of licenses.
-   Percentage rate.

Why?

-   Precision matters.
-   Value must be exact.

Avoid when:

-   The number has formatting rules (phone, IBAN, BIC).
-   Users are more likely to think in ranges than exact values.

### When the user is selecting an approximate value or a range

Use  component.

Use for:

-   Budget filter.
-   Price filtering in search results.
-   Age range selection.
-   Performance tuning.

Why?

-   Encourages exploration.
-   Works well for filtering.
-   Suitable when precision is not critical.

Avoid when:

-   Legal, financial, or contractual precision is required.
-   The user must enter a specific numeric value.

### When users must choose one option and all options should be visible

Use  buttons.

Use for:

-   Choosing a subscription plan (e.g., Basic / Pro / Enterprise).
-   Selecting delivery method.
-   Choosing payment type.

Why?

-   All options are visible.
-   Encourages comparison.
-   Reduces hidden choices.
-   Recommended when there are between 2 and 5 options.
-   Each option has important contextual differences.

### When users must choose one option from a list

#### Select vs Combobox

When a user must select a single option from a predefined list, the choice between **Select** and **Combobox** depends mainly on the size and complexity of the list.

#### Use Select

Use  when the list is limited and easy to scan visually.

Typical situations:

-   Selecting a status (Draft / Pending / Approved).
-   Choosing a priority level (Low / Medium / High).
-   Selecting a department in a small organization.

**Select** is appropriate when:

-   The number of options is moderate.
-   Users can easily scroll and recognize the right option.
-   Search is not necessary.

#### Use Combobox

Use  when the list is long or when search significantly improves usability.

Typical situations:

-   Selecting a country from a global list.
-   Choosing a city.
-   Assigning a user from a large organization.
-   Selecting a product from a large catalog.

**Combobox** is appropriate when:

-   The dataset is large.
-   Users are likely to know what they are looking for.
-   Scrolling through the entire list would be inefficient.

### When users can select multiple independent options

Use .

Use for:

-   Selecting product features.
-   Choosing notification preferences.
-   Accepting multiple agreements.

Why?

-   Clearly communicates independence.
-   No mutual exclusion implied.
-   Good for visible, short lists.

### When the user is enabling or disabling a setting

Use  component.

Use for:

-   Enable dark mode.
-   Activate notifications.
-   Turn on auto-renewal.

Why?

-   Communicates immediate on/off state.
-   Best for persistent settings.

Avoid when:

-   The action is critical or legally binding.
-   The user must explicitly confirm something important.

In those cases, use a .

### When the user must select a specific date

Use  component.

Use for:

-   Booking a meeting.
-   Setting a deadline.
-   Scheduling delivery.

Why?

-   Prevents format errors.
-   Reduces cognitive load.
-   Ensures valid date selection.

Consider alternatives when:

-   Only month and year are required.
-   The user frequently types dates manually (advanced users).

### When the user must provide supporting material

Use  component.

Use for:

-   Uploading identity documents.
-   Adding attachments to a request.
-   Submitting supporting evidence.
-   Uploading invoices.

Why?

-   Allows users to provide official or visual proof that cannot be reliably captured in structured fields.
-   The content may need to be reviewed manually.
-   Reproducing the information through fields would be incomplete or unreliable.

Avoid when:

-   The information can be captured through structured inputs (text, number, date, select).
-   The upload is used to compensate for missing inputs.

## Implementing a Form

---

ODS form elements can either be [controlled or uncontrolled](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components) .

You can use each form element directly in your own form, or wrap them using the  component.

## Form Field

---

Using  component will enforce layout, design, behavior and accessibility through a few dedicated components:

-   `FormFieldLabel`
-   `FormFieldHelperText`
-   `FormFieldError`

You can also deport the `invalid` prop from the form element to the `FormField` component which will handle the error display automatically.

```tsx
<FormField invalid={ someCondition }>
  <FormFieldLabel>
    Input:  </FormFieldLabel>
  <Input name="input" />
  {/* Displayed only if invalid on FormField is true */}
  <FormFieldError>
    Error message  </FormFieldError>
</FormField>
```

## Uncontrolled Form

---

Uncontrolled component will not have the `value` property set.

You can use the `defaultValue` property to initialize the component with a value.

Example of uncontrolled form:

```tsx
import { Button, FormField, FormFieldLabel, Input } from '@ovhcloud/ods-react';
import { type FormEvent, useRef } from 'react';
const UncontrolledForm = () => {
  const formRef = useRef<HTMLFormElement>(null);
  function onSubmit(e: FormEvent) {
    e.preventDefault();
    if (formRef.current) {
      const formData = new FormData(formRef.current);
      console.log(formData);
    }
  }
  return (
    <form
      onSubmit={ onSubmit }
      ref={ formRef }>
      <FormField>
        <FormFieldLabel>
          Input:        </FormFieldLabel>
        <Input
          defaultValue="default"
          name="input" />
      </FormField>
      <Button type="submit">
        Submit button      </Button>
    </form>
  );
};
```

## Controlled Form

---

Controlled components will manage their `value` prop on their own and react to value change events.

Example of controlled form:

```tsx
import { Button, FormField, FormFieldLabel, Input } from '@ovhcloud/ods-react';
import { type ChangeEvent, useState } from 'react';
const ControlledForm = () => {
  const [inputValue, setInputValue] = useState('default');
  function onInputChange(e: ChangeEvent<HTMLInputElement>) {
    setInputValue(e.target.value);
  }
  function onSubmit(e: FormEvent) {
    e.preventDefault();
    console.log('input: ', inputValue);
  }
  return (
    <form onSubmit={ onSubmit }>
      <FormField>
        <FormFieldLabel>
          Input:        </FormFieldLabel>
        <Input
          name="input"
          onChange={ onInputChange }
          value={ inputValue } />
      </FormField>
      <Button type="submit">
        Submit button      </Button>
    </form>
  );
};
```

## Using form library

---

We'll demonstrate here a few form librairy, but you can plug ODS form elements with any library you like.

Regardless of the library used, you can also use a schema validator to manage your data (like [yup](https://github.com/jquense/yup) or [zod](https://zod.dev/) ).

### react-hook-form

[React Hook Form](https://react-hook-form.com) is thought around native uncontrolled form elements (using `register`).

These will mork on most ODS components, but some specific one may still need to rely on the lib `Controller` component.

```tsx
import { Button, Datepicker, DatepickerContent, DatepickerControl, FormField, FormFieldLabel, Input } from '@ovhcloud/ods-react';
import { Controller, useForm } from 'react-hook-form';
type FormData = {
  datepicker: string,
  input: string,
}
const FormHookForm = () => {
  const {
    control,
    formState: { errors },
    handleSubmit,
    register,
    setValue,
  } = useForm<FormData>({ mode: 'onBlur' });
  function onSubmit(data: FormData): void {
    console.log(data);
  }
  return (
    <form onSubmit={ handleSubmit(onSubmit) }>
      <FormField invalid={ !!errors.input }>
        <FormFieldLabel>
          Input:        </FormFieldLabel>
        <Input { ...register('input') } />
      </FormField>
      <Controller
        control={ control }
        name="datepicker"
        render={ ({ field} ) => (
          <FormField invalid={ !!errors.datepicker }>
            <FormFieldLabel>
              Datepicker:            </FormFieldLabel>
            <Datepicker onValueChange={ ({ value }) => setValue(field.name, value) }>
              <DatepickerControl />
              <DatepickerContent />
            </Datepicker>
          </FormField>
        )} />
      <Button type="submit">
        Submit button      </Button>
    </form>
  );
}
```

### formik

Here is another example using [Formik](https://formik.org) library.

```tsx
import { Button, Datepicker, DatepickerContent, DatepickerControl, FormField, FormFieldLabel, Input } from '@ovhcloud/ods-react';
import { useFormik } from 'formik';
type FormData = {
  datepicker: string,
  input: string,
}
const FormFormik = () => {
  const formik = useFormik<FormData>({
    onSubmit: (values) => {
      console.log(values);
    },
    validateOnMount: true,
    validationSchema,
  });
  return (
    <form onSubmit={ formik.handleSubmit }>
      <FormField invalid={ formik.touched.input && !!formik.errors.input }>
        <FormFieldLabel>
          Input:        </FormFieldLabel>
        <Input
          name="input"
          onBlur={ formik.handleBlur }
          onChange={ formik.handleChange } />
      </FormField>
      <FormField invalid={ formik.touched.datepicker && !!formik.errors.datepicker }>
        <FormFieldLabel>
          Datepicker:        </FormFieldLabel>
        <Datepicker
          name="datepicker"
          onBlur={ formik.handleBlur }
          onValueChange={ ({ value }) => {
            formik.setFieldValue('datepicker', value);
          }}>
          <DatepickerControl />
          <DatepickerContent />
        </Datepicker>
      </FormField>
      <Button type="submit">
        Submit button      </Button>
    </form>
  );
}
```

## Internationalization

---

ODS does not embed any i18n library, as most of the translatable contents are coming from the integration side.

There are two exceptions, components with full embedded translation and components with default translation on some accessibility attribute.

## Embedded translation

---

So far, only the following components internally manage a complete translation system:

-   
-   

Both are relying on the native [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) feature.

They do de-facto support the same locales as [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) .

## ODS specific translation

---

Although you have complete control on label translation when using the ODS components, there are a few internal elements that need a textual description for accessibility purpose.

For example, take the  component:

Here, the close button needs to have the right `aria-label` value set for accessibility compliance. Though the element is not part of the composable components, it is internal.

By default, ODS will provide a default `aria-label` message for all locales that we currently support. So if you use the component as-is, it will be accessible already.

In case you want to change the locale to one supported by ODS, you can use the `locale` attribute of the component.

```tsx
// This will change the close button aria-label to the french version
<Message locale="fr">
  <MessageBody>
    Mon message  </MessageBody>
</Message>
```

If the `locale` is not set, the component will try to use the navigator languages by default.

In case you want to change to a locale not supported by ODS, or you want to override the default messages, you can set your own translation using the `i18n` attribute of the component.

```tsx
// This will change the close button aria-label to your own string
<Message i18n={{ [MESSAGE_I18N.closeButton]: 'Remove the notification' }}>
  <MessageBody>
    Mon message  </MessageBody>
</Message>
```

## ODS supported locale

---

Current locales supported by ODS:

-   de
-   en
-   es
-   fr
-   it
-   nl
-   pl
-   pt

## Roadmap

---

## ## A collection of assets, guidelines and UI components for building consistent user experiences across OVHcloud products.

[Design Guidelines](https://zeroheight.com/6fc8a63f7/p/533db0-ovhcloud-design-system) [GitHub repository](https://github.com/ovh/design-system)

## [12.1.3](https://ovh.github.io/design-system/v12.1.3/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) to [13.0.0](https://ovh.github.io/design-system/v13.0.0/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) ([compare](https://github.com/ovh/design-system/compare/release/12.1...release/13.0))

## Select width

---

The Select component fixed width when non flex has been removed.

```css
width: calc(var(--ods-size-stack-08) * 11)
```

Should you need any width, you will have to set it on your component.

## [13.0.1](https://ovh.github.io/design-system/v13.0.1/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) to [14.0.0](https://ovh.github.io/design-system/v14.0.0/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) ([compare](https://github.com/ovh/design-system/compare/release/13.0...release/14.0))

## Tile interactive attribute

---

The Tile component `interactive` attribute has been removed.

```typescript
interactive?: boolean;
```

If you are using `interactive` attribute on a Tile in your project, you should remove it.

## [14.1.1](https://ovh.github.io/design-system/v14.1.1/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) to [15.0.0](https://ovh.github.io/design-system/v15.0.0/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) ([compare](https://github.com/ovh/design-system/compare/release/14.0...release/15.0))

## Text

---

### Dependency

Add a dependency to `@ovhcloud/ods-component-text` or `@ovhcloud/ods-components`.

### Import

Replace any imports like the followings:

```typescript
// Component
import { OsdsText } from '@ovhcloud/ods-stencil/components/text'
import { OsdsText } from '@ovhcloud/ods-stencil/components/text/react'
import { OsdsText } from '@ovhcloud/ods-stencil/components/text/vue'
// Enums
import { OdsTextLevel } from '@ovhcloud/ods-core'
import { OdsTextLevelList } from '@ovhcloud/ods-core'
import { OdsTextSize } from '@ovhcloud/ods-core'
import { OdsTextSizeList } from '@ovhcloud/ods-core'
// Interfaces
import { OdsTextAttributes } from '@ovhcloud/ods-core'
```

to:

```typescript
// Component
import { OsdsText } from '@ovhcloud/ods-component-text'
import { OsdsText } from '@ovhcloud/ods-component-text/react'
import { OsdsText } from '@ovhcloud/ods-component-text/vue'
// Enums
import { ODS_TEXT_LEVEL } from '@ovhcloud/ods-component-text'
import { ODS_TEXT_LEVELS } from '@ovhcloud/ods-component-text'
import { ODS_TEXT_SIZE } from '@ovhcloud/ods-component-text'
import { ODS_TEXT_SIZES } from '@ovhcloud/ods-component-text'
// Interfaces
import type { OdsTextAttribute } from '@ovhcloud/ods-component-text'
```

Following interfaces has been removed:

-   `OdsText`
-   `OdsTextEvents`
-   `OdsTextMethods`
-   `OdsTextLevelUnion`
-   `OdsTextSizeUnion`

## Textarea

---

### Dependency

Add a dependency to `@ovhcloud/ods-component-textarea` or `@ovhcloud/ods-components`.

### Import

Replace any imports like the followings:

```typescript
import { OsdsTextarea } from '@ovhcloud/ods-stencil/components/textarea'
import { OsdsTextarea } from '@ovhcloud/ods-stencil/components/textarea/react'
import { OsdsTextarea } from '@ovhcloud/ods-stencil/components/textarea/vue'
// Enums
import { OdsTextAreaSize } from '@ovhcloud/ods-core'
import { OdsTextAreaSizeList } from '@ovhcloud/ods-core'
// Interfaces
import { OdsTextAreaAttributes } from '@ovhcloud/ods-core'
import { OdsTextAreaEvents, OdsTextAreaChangeEventDetail } from '@ovhcloud/ods-core'
import { OdsTextAreaMethods } from '@ovhcloud/ods-core'
```

to:

```typescript
// Component
import { OsdsTextarea } from '@ovhcloud/ods-component-textarea'
import { OsdsTextarea } from '@ovhcloud/ods-component-textarea/react'
import { OsdsTextarea } from '@ovhcloud/ods-component-textarea/vue'
// Enums
import { ODS_TEXT_LEVEL } from '@ovhcloud/ods-component-textarea'
import { ODS_TEXT_LEVELS } from '@ovhcloud/ods-component-textarea'
import { ODS_TEXT_SIZE } from '@ovhcloud/ods-component-textarea'
import { ODS_TEXT_SIZES } from '@ovhcloud/ods-component-textarea'
// Interfaces
import type { OdsTextAreaAttribute } from '@ovhcloud/ods-component-textarea'
import type { OdsTextAreaEvent, OdsTextAreaChangeEvent } from '@ovhcloud/ods-component-textarea'
import type { OdsTextAreaMethod } from '@ovhcloud/ods-component-textarea'
```

Following interfaces has been removed:

-   `OdsTextArea`
-   `OdsTextAreaBehavior`
-   `OdsTextAreaSizeUnion`

## [15.0.1](https://ovh.github.io/design-system/v15.0.1/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) to [16.0.0](https://ovh.github.io/design-system/v16.0.0/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) ([compare](https://github.com/ovh/design-system/compare/release/15.0...release/16.0))

Presenting all changes, components per components will cause a very long and hard to use changelog.

Instead we'll present you the fundamental changes that have been applied for all of the existing components.

The same logics has been applied to each components.

If you have any issue finding a specific change, feel free to contact us directly and we'll help you migrate.

## All Components

---

### Dependency

To use the component add a dependency to either:

-   `@ovhcloud/ods-component-<component>` to import only the specific component.
-   `@ovhcloud/ods-components` to import all components.

### Import

Replace any imports like the followings:

```typescript
// Previously component were imported using one of:
import { OsdsSpinner } from '@ovhcloud/ods-stencil/components/spinner'
import { OsdsSpinner } from '@ovhcloud/ods-stencil/components/spinner/react'
import { OsdsSpinner } from '@ovhcloud/ods-stencil/components/spinner/vue'
// Enums were imported from the ods-core library
import { OdsSpinnerSize, OdsSpinnerSizeList } from '@ovhcloud/ods-core'
// Interfaces were imported from the ods-core library
import { OdsSpinnerAttributes } from '@ovhcloud/ods-core'
```

to:

```typescript
// Component are now imported using one of:
import { OsdsSpinner } from '@ovhcloud/ods-component-spinner'
import { OsdsSpinner } from '@ovhcloud/ods-component-spinner/react'
import { OsdsSpinner } from '@ovhcloud/ods-component-spinner/vue'
// Enums are now imported directly from the component
import { ODS_SPINNER_SIZE, ODS_SPINNER_SIZES } from '@ovhcloud/ods-component-spinner'
// Interfaces are now imported directly from the component
import type { OdsSpinnerAttribute } from '@ovhcloud/ods-component-spinner'
```

Some types have been removed from ODS, as they were empty or not used, if you can't find a type you were using, please contact us to see where it has been moved or how to replace it.

## [16.6.0](https://ovh.github.io/design-system/v16.6.0/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) to [17.0.0](https://ovh.github.io/design-system/v17.0.0/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) ([compare](https://github.com/ovh/design-system/compare/release/16.0...release/17.0))

Main focus on this version has been on simplifying ODS integration in existing projects (React, Vue, JS, ...).

If you have any issue finding a specific change, feel free to contact us directly and we'll help you migrate.

## All Components

---

### Dependency

We now only publish one `ods-components` library that embed all the ODS components.

You need to remove any component specific dependencies, ex:

```text
"ods-component-button": "16.6.0" // should be removed
```

Instead use only one dependency:

```text
"ods-components": "17.0.0"
```

### Import

You can see more details in the updated 

#### Native webcomponents

Using lazy-loading:

```typescript
import { defineCustomElements } from '@ovhcloud/ods-components/dist/loader';
defineCustomElements();
```

As module:

```typescript
import { defineCustomElement as defineButton } from '@ovhcloud/ods-components/dist/components/osds-button';
defineButton();
```

#### React components

```typescript
import { OsdsButton, OsdsText } from '@ovhcloud/ods-components/react';
```

#### Vue components

```typescript
import { OsdsButton, OsdsText } from '@ovhcloud/ods-components/vue';
```

#### Types and constants

All types and constants are accessible from the `ods-components` library:

```typescript
import type { OdsButtonAttribute } from '@ovhcloud/ods-components'
import { ODS_BUTTON_SIZE, ODS_BUTTON_SIZES } from '@ovhcloud/ods-components';
```

### About `ods-common-xxx` libs

We're trying to remove the need to add any `ods-common-xxx` lib to any project using ODS.

For now, you will still need to add `ods-common-theming` as a dependency to access the `ODS_THEME_COLOR_INTENT` values.

If you still ended up adding `ods-common-core` in your project, please give us the feedback so that we work on removing that need.

## [17.2.2](https://ovh.github.io/design-system/v17.2.2/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) to [18.0.0](https://ovh.github.io/design-system/v18.0.0/?path=/story/ovhcloud-design-system-what-s-new-changelog--page) ([compare](https://github.com/ovh/design-system/compare/release/17.0...release/18.0))

If you have any questions or issues regarding the new version, feel free to reach us directly.

## Libraries

---

The following libs have been removed:

-   @ovhcloud/ods-cdk
-   @ovhcloud/ods-common-core
-   @ovhcloud/ods-common-stencil
-   @ovhcloud/ods-common-testing
-   @ovhcloud/ods-common-theming
-   @ovhcloud/ods-theme-blue-jeans

If you were using part of them, please reach out to us so that we can get more information about your usage.

To use the new theme, replace `ods-theme-blue-jeans` with the following:

```typescript
import '@ovhcloud/ods-themes/default';
```

## Design tokens

---

Design tokens are now accessible through the `@ovhcloud/ods-themes` lib.

The list of available tokens has been updated with the latest design changes and match what you may find on the Figma screens.

## Components

---

As each component was refactored, there are too many changes to list everything here.

The main common breaking change is the following: `osds-xxx` component is now `ods-xxx`

A complete list of changes and a guide about how to migrate is available per component in the v18 documentation.

Use the top-left version selector to move to the latest v18.x documentation where you'll find the latest up-to-date information.

## Migrate to v19

---

Version 19 of ODS is not the direct follow-up of previous versions, it is a new library built 100% on top of [React](https://react.dev) .

Previous versions were providing a React version of each components too, but there were in fact a wrapping of the Web Component. Thus it was designed with the constraints related to Web Component (no virtual DOM, relying on DOM selector, ...).

With this new version we got rid of those constraints, which gave us some benefits like:

-   component composition.
-   easier style customization.
-   easier React integration (no more `ref` needed, nor literal templates).

We've also alleviate a lot of previous component restrictions, as most of them now accept React nodes instead of fixed property.

For example, Button will now accept any node (text, text + icon, triple icon, ...) and won't force you to set only a label and an optional icon.

Moving out of Web Component also means getting rid of the Shadow DOM. This means you now have direct access to the component DOM and it's easier to add you own style. Though this also means it is easier to break the component rendering, and some custom selectors may break from version to version (see  to know more).

Regarding components, we tried to stay as close as possible to their previous interface while keeping them easy to integrate. Though, the way you'll integrate them in your React app should be rethought, to ensure you use them as real React components and not wrapped Web Components anymore.

To check how to start with this new version, please follow the  documentation.

## What's new ?

---

## v19.7.0

---

### New components:

**Avatar**

.

**DataTable**

.

**FileThumbnail**

.

**Markdown**

.

**MessageBubble**

.

**PromptInput**

.

**QueryFilter**

.

### New recipes:

**Data Grid with Query Filter**

.

**Chat**

.

### Feature updates:

**Compound components**

ODS now offers different ways to be imported in your project, either through the main export that provides everything (like in previous version) or per-component.

The current way:

```tsx
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ovhcloud/ods-react';
<Accordion>
  <AccordionItem value="0">
    <AccordionTrigger>
      Hello World!    </AccordionTrigger>
    <AccordionContent>
      Lorem ipsum dolor sit amet.    </AccordionContent>
  </AccordionItem>
</Accordion>
```

The per-component way:

```tsx
import { Accordion } from '@ovhcloud/ods-react/accordion';
<Accordion>
  <Accordion.Item value="0">
    <Accordion.Trigger>
      Hello World!    </Accordion.Trigger>
    <Accordion.Content>
      Lorem ipsum dolor sit amet.    </Accordion.Content>
  </Accordion.Item>
</Accordion>
```

We did test on a bunch of different bundler configuration to ensure that the tree-shaking is still working as expected and that existing configuration will not be impacted.

**Clipboard**

-   New attribute `positionerStyle` has been added.

**Code**

-   Copy button is now sticky when content is scrollable.
-   New attribute `positionerStyle` has been added.

**Combobox**

-   New attributes `open`, `defaultOpen` and `onOpenChange` have been added.
-   New attribute `overlayConfig` has been added, with `flip` and `sameWidth` options.

**Datepicker**

-   New attribute `onOpenChange` has been added.
-   New attribute `overlayConfig` has been added, with a `flip` option.

**Divider**

-   New attribute `orientation` has been added to support vertical dividers.

**Helpers**

-   New helper function `formatRelativeTime` has been added.

**Icon**

-   Add the following new icons: `accessibility`, `accessibility-full`, `paperclip`, `pause`, `pause-full`, `phone`, `play`, `play-full`, `stop`, `stop-full`, `theme`.

**Menu**

-   New attribute `overlayConfig` has been added, with `flip`, `gutter` and `sameWidth` options.

**Pagination**

-   New page selector feature has been added.

**Popover**

-   New attribute `overlayConfig` has been added, with a `flip` option.

**ProgressBar**

-   New `indeterminate` value has been added.

**Select**

-   New attributes `open`, `defaultOpen` and `onOpenChange` have been added.
-   New attribute `overlayConfig` has been added, with a `flip` option.

**Tooltip**

-   New attribute `overlayConfig` has been added, with `flip`, `gutter` and `sameWidth` options.

### Bug fixes:

**Accordion**

-   Trigger text can now be selected.

**Cart**

-   Remove button is now aligned with the label.
-   Item price is now correctly aligned to the right.

**Combobox**

-   Caret click no longer triggers a form submit.

**Datepicker**

-   Text input display bug has been fixed.

**Font**

-   Variable weight declaration has been fixed.

**Meter**

-   Track container no longer overflows.

**Table**

-   Sticky header border issue on scroll has been fixed.

**Textarea**

-   Placeholder now uses the theme color.

### Documentation improvements:

-   Tech anatomy sections are now dynamic.
-   Icon gallery has been improved.

## v19.6.1

---

### Feature updates:

**Menu**

-   handle disabled state on MenuItem.

### Bug fixes:

**Combobox**

-   Hide caret button from screen readers.

**FileUpload**

-   Update file item size color to a valid contrasted one.

## v19.6.0

---

### New components:

**Cart**

.

**Menu**

.

### New recipes:

**Dashboard Card**

.

### Design updates:

**Theme**

-   New token `--ods-theme-chart-axis-color` has been added.
-   New token `--ods-theme-chart-background-color` has been added.
-   New token `--ods-theme-chart-grid-color` has been added.
-   New token `--ods-theme-chart-legend-color` has been added.
-   New token `--ods-theme-chart-reference-line-color` has been added.
-   New token `--ods-theme-chart-tick-color` has been added.

**Combobox**

-   A caret is now displayed on the right side of the control.

**Toaster**

-   Toasts are now animated.

### Feature updates:

**Icon**

-   Add the following new icons: brain, diagnostic, handshake, headphones, keyboard, reddit, ssl-key, snowflake, suitcase, target, user-full
-   `user` icon has been changed, previous icon is now called `user-full`.
-   `mic-off` and `pin-off` icons have been updated.

**Tag**

-   New attribute `icon` has been added.

### Bug fixes:

**Accordion**

-   Content style now inherit only bottom border-radius.

**Combobox**

-   `disable` has been remove from `ComboboxOptionItem` and `ComboboxGroupItem` interfaces, as they weren't doing anything.

**Menu**

-   Arrow tip dom has been changed to inherit content background color.

**Popover**

-   Arrow tip dom has been changed to inherit content background color.

**Tooltip**

-   Arrow tip dom has been changed to inherit content background color.

### Documentation improvements:

-   A new guide `Form Guidelines` has been added to the documentation.
-   A new guide `Chart Guidelines` has been added to the documentation.
-   A new separated LLM documentation for every components has been added.
-   Fix mixin import example.
-   Fix some code examples that weren't displaying false boolean attribute.

## v19.5.0

---

### New components:

**Editable**

.

**Logo**

.

**Tile**

.

### New recipes:

**Config Tile**

.

**Feature List Product Card**

.

### Design updates:

**Theme**

-   New token `--ods-theme-brand-color` has been added.
-   `ods-color-promotion` has been updated to a new value.

**Accordion**

-   Now has a default border radius.

**Badge**

-   `--ods-badge-text-color-promotion` token as been updated to match new promotion color.
-   Badge sizes and padding have been adjusted.

**Modal**

-   Header is now a colorless element that can contain actual content.

**Tag**

-   Tag sizes and padding have been adjusted.

### Feature updates:

**Overlays z-index** In previous version of ODS, `Drawer` and `Modal` z-indexes were set to a lesser value than other overlay elements (popover, select, ...). This was allowing those to display correctly when set inside a modal. But this was causing an issue when, for example, a popover was kept open behind a modal, as it will be visible above the modal backdrop.

Starting from this version, `Drawer` and `Modal` z-indexes are now greater than other overlay elements. This may cause issue with your current usage of overlay elements inside `Modal`.

To use such element correctly, you'll have to set the `createPortal={ false }` on overlay elements inside `Modal`. You can see some examples on the .

We've also added a `positionerStyle` attribute on all overlay elements to allow you to override the positioner style and set up your own `z-index` values if needed.

**Accordion**

-   New attribute `expandIconPosition` has been added.

**Datepicker**

-   New attribute `positionerStyle` has been added.

**Drawer**

-   New attribute `positionerStyle` has been added.

**Form Field**

-   New composition component `FormFieldLabelSubLabel` has been added.

**Modal**

-   New attribute `backdropStyle` has been added.
-   New attribute `positionerStyle` has been added.
-   New composition component `ModalHeader` has been added.
-   You can now setup your own modal header with a custom node.
-   [DEPRECATED] Color attribute is not used anymore, as modal header doesn't have any color anymore.

**Popover**

-   New attribute `positionerStyle` has been added.

**Select**

-   New attribute `positionerStyle` has been added.

**Tooltip**

-   New attribute `positionerStyle` has been added.

### Recipe changes:

**Feature List** **Order Button** **Product Card**

-   Style have updated with latest design requirements, mostly text changes and tooltip alignment.

### Documentation improvements:

A new page `Roadmap` has been added to the documentation. You can refer to it to have an overview of what we're working on and what you could expect for the future ODS releases. Please note that this page may be updated in case of major priority changes, outside of an ODS version release.

-   Medium examples have been updated as they were the same as `Logo`.

### Miscellaneous:

-   Remove console warning about `Modal` color depreciation, as it was print even if color wasn't set.

## v19.4.1

---

### Bug fixes:

**Datepicker**

-   Overriding `display` on `DatepickerContent` now won't cause any closing issue.

**Drawer**

-   Overriding `opacity` on `DrawerContent` now won't cause any closing issue.

**Modal**

-   Overriding `display` on `ModalContent` now won't cause any closing issue.

**Popover**

-   Overriding `display` on `PopoverContent` now won't cause any closing issue.

**Range**

-   Fix infinite loop that may occurs on dual range.

**Select**

-   Overriding `display` on `SelectContent` now won't cause any closing issue.

**Tooltip**

-   Overriding `display` on `TooltipContent` now won't cause any closing issue.

## v19.4.0

---

### New features:

**Helpers**

`@ovhcloud/ods-react` package now expose the following helper functions:

-   `formatPrice`: useful to format an amount into a price with currency for a given locale.

You can find more information about helpers on the new dedicated section in the documentation.

**Recipes**

Recipes are ready-to-use UI patterns built with ODS components. They provide pre-implemented code snippets for common use cases, helping you build consistent interfaces faster. Each recipe may include multiple implementations, so you can choose the styling approach that fits your project.

We're still iterating on the whole recipe system to ensure it fits integrator needs. Feel free to share with us your feedback with your own experience while using those recipes.

You can find more information about recipes on the new dedicated section in the documentation.

### New components:

**Button Group**

.

**Kbd**

.

### Design updates:

**Tabs**

This change may impact your layouts.

-   Default tabs height have been increased.

### Feature updates:

**Icon**

-   Add 17 new icons.

**Switch**

-   [DEPRECATED] Whole component is now deprecated, you can use the new Tabs `switch` variant instead.

**Tabs**

-   New attribute `size` has been added.
-   New attribute `variant` has been added.

### Bug fixes:

**AI Agents**

-   LLM files now contains all the storybook relevant pages.

**Code**

-   Highlighter instance is now correctly cleaned on unmount.

**Combobox**

-   Prevent content close on scrollbar click.
-   Active option is now automatically scrolled into view.

**FileUpload**

-   Use correct role on file item error.

**Range**

-   Prevent thumb focus when value is changed by external sources.

**Select**

-   Value text and placeholder are now ellipse by default.

## v19.3.0

---

### New components:

**Toaster**

### Design updates:

ODS theme design tokens have been reworked to be more semantic. It is now easier to understand how each token should be used. Each component have been updated accordingly and are now more normalized design-wise. Each component now also expose custom tokens that may be used to override rendering on a component-level.

All those changes are the first major step towards a complete reworked theme that aims to provide more consistency, easier customization and understanding.

[DEPRECATED] Previous design tokens should be replaced by the corresponding theme tokens.

**Accordion**

-   Border color has been changed and is now the same for every component states.

**Combobox**

-   `invalid` overlay now uses the critical color.

**File Upload**

-   A more compact version is now available (using `variant` attribute).
-   Existing layout has been updated to be more compact.

**Message**

-   Light messages now have a colored border.

**Range**

-   Track color now is the same as other track elements.
-   Cursor has been changed to default (as in native).
-   Thumb background color change on hover/focus/drag has been removed.

**Select**

-   `invalid` overlay now uses the critical color.
-   Groups are now separated by a border.

**Skeleton**

-   Default background color is now slightly darker.

**Table**

-   Default `min-width` has been removed on mobile view.

**Text**

-   Font size now uses `rem` instead of `px`. This will only impact app where the root element `font-size` is not `16px`.

**Toggle**

-   Hover effect on `off` label has been removed.

### Feature updates:

**Code**

-   New attribute `highlighter` has been added.
-   Documentation has been updated with how to setup a custom highlighter.

**Datepicker**

-   `value` now also accepts `null` as a value.
-   `DatepickerValueChangeDetail` `value` and `valueAsString` now may be `null`.

**Divider**

-   [DEPRECATED] Color attribute will now always be primary, if you need another color, prefer overriding it using css.

**File Upload**

-   New attribute `variant` has been added.

**Text**

-   New attribute `disabled` has been added.
-   New preset `small` has been added.

### Bug fixes:

**Combobox**

-   Clicking another combobox while focused on another one, now correctly closes the first one.
-   Controlled value changed externally is now correctly reflected.

**Datepicker**

-   Clear when controlled now works as expected.

**Link**

-   Disabled links do not have an underline animation when hovered.

**Tabs**

-   `tablist` role now correctly contains only valid children.
-   Now doesn't automatically scroll to the component on render.

### Documentation improvements:

A new tool has been added to the documentation, the `Theme Generator`. This allows users to experiment with the ODS tokens directly in the storybook. You can preview how each design tokens will impact the components, generate your own color palettes and export/import you custom theme.

ODS documentation is now available as LLM files to help AI tools to better understand our library and its usage.

-   Add a theme selector on the top left menu.
-   Implement dark mode documentation.
-   New section `AI Agents` added to the documentation.
-   Design Tokens have been updated and completed with color palettes.
-   All technical pages have been updated with components custom CSS variables.
-   Icon `All` page search control now accepts special character.
-   Update FAQ.

## v19.2.1

---

### Feature updates:

**Combobox**

-   New attribute `customFilter` has been added.

### Bug fixes:

**Button**

-   Use correct border radius for `sm` size.

**Input**

-   Normalize border style on hover & focus.

**Password**

-   Normalize border style on hover & focus.

**Tabs**

-   Container bottom border takes the whole width.

### Documentation improvements:

-   Update FAQ.
-   Remove ToC on Changelog page.

### Miscellaneous:

**Tabs**

-   Add a console warning when no `defaultValue` or `value` are set.

## v19.2.0

---

### New components:

**Meter**

**TreeView**

### Font changes:

Font files are now accessible as actual files instead of base64 conversion. This will reduce the size of the ODS style bundle and allow browser resource loading parallelization.

Thanks to [Ennoriel](https://github.com/ennoriel) for the contribution.

**@ovhcloud/ods-react**

-   Now exposes a css reset file (using `import '@ovhcloud/ods-react/normalize-css';`)

**@ovhcloud/ods-themes**

-   Now exposes css without fonts (using `import '@ovhcloud/ods-themes/default/css';`)
-   Now exposes font-faces separately (using `import '@ovhcloud/ods-themes/default/fonts';`)

### Feature updates:

**@ovhcloud/ods-themes**

-   Now exposes design tokens as JSON (using `import '@ovhcloud/ods-themes/default/tokens';`)

**File Upload**

-   Improve i18n default translations.

**Icon**

-   Add 40 new icons.

**Password**

-   Improve i18n default translations.

**Phone Number**

-   Improve i18n default translations.

**Popover**

-   New attribute `autoFocus` has been added.
-   New attribute `gutter` has been added.
-   New attribute `sameWidth` has been added.
-   New attribute `onPositionChange` has been added.
-   New attribute `triggerId` has been added.

**Range**

-   New attribute `displayBounds` has been added.
-   New attribute `displayTooltip` has been added.
-   Ticks now accepts objects with custom labels.
-   Range component size is now correctly computed, it will fit better in the page rendering flow.

**Tabs**

-   New attribute `withArrows` has been added.

**Timepicker**

-   Improve i18n default translations.

### Bug fixes:

**Combobox**

-   Has been rewritten internally, but the usage stays the same.
-   Fix selection behavior issues.
-   Add style on selected element.
-   Fix missing style on group separator.

**Input**

-   Clearable now always appears correctly when controlled.

**Quantity**

-   Fix default value stuck when used inside a FormField.

**Range**

-   Update ticks position computation to match thumb size.

### Documentation improvements:

A new tool has been added to the documentation, the `Code sandbox`. This allows users to experiment with the ODS library directly in the storybook. You can also share your sandbox code with others. This is especially useful when trying to reproduce some issue you encounter, but in a smaller dedicated context.

-   New section `Tools` added to the documentation.
-   A sandbox shortcut has been added to every examples.
-   New dedicated page about Tailwind integration.
-   Add badges when relevant near navigation menu items.
-   Update "Get Started" with fonts changes.
-   Update "Apply ODS Style" with CSS reset.
-   Update GitHub repository contributing page.
-   Update FAQ with contribution information.
-   Multiple typo fixes.

### ODS dependencies upgrade:

-   ArkUI bumps from `5.12.0` to `5.25.1`.

## v19.1.0

---

### Accessibility improvements:

#### a11y

This version focuses on improving the accessibility for each component.

We've implemented all the internal requirements on each component, validated using [axe-core](https://github.com/dequelabs/axe-core) and [lighthouse](https://www.npmjs.com/package/lighthouse) .

Though, ensuring accessibility for your context does require you to use the right `aria`/`role`/... attribute at the right place.

To help you with that, we've added:

-   a  best practices.
-   a dedicated `Accessibility` section on each component `Documentation` page.

#### i18n

Some components provide internal features that need an explicit context. For example the clear button on the `Input` which require an `aria-label` describing that the button will clear the value.

We do provide default values for each supported locales to ensure that integrating ODS component as is will already convey the minimal information to impaired users.

You can customize those value further with your own translations.

See the related storybook  for more information.

### Design updates:

Some design changes may impact your layouts.

**Breadcrumb**

-   `fontSize` is now inherited and no more fixed to `0.875rem`.

**Checkbox**

-   Added minimal vertical spacing on group.
-   Checkbox size is now `16px`.

**Design Tokens**

-   New color variants `-025` and `-075` have been added.

**Progress Bar**

-   Now defaults to full width.

**Tag**

-   `md` height is now `24px` by default.

### Feature updates:

**All components**

-   A new attribute `data-ods` has been added to each exposed component, allowing them to be identified in the DOM.

**Breadcrumb**

-   New attribute `i18n` has been added.
-   New attribute `locale` has been added.

**Button**

-   Improve button contrast for accessibility.

**Clipboard**

-   New attribute `i18n` has been added.
-   New attribute `locale` has been added.

**Combobox**

-   Added `customRendererData` on combobox items.
-   New attribute `i18n` has been added.
-   New attribute `locale` has been added.

**Datepicker**

-   New attribute `i18n` has been added.
-   New attribute `locale` has been added.

**File Upload**

-   New attribute `i18n` has been added on `FileUploadItem`.
-   New attribute `locale` has been added.

**Input**

-   New attribute `i18n` has been added.
-   New attribute `locale` has been added.

**Message**

-   Improve color contrast for accessibility.
-   New attribute `i18n` has been added.
-   New attribute `locale` has been added.

**Modal**

-   New attribute `i18n` has been added.
-   New attribute `initialFocusedElement` has been added.
-   New attribute `locale` has been added.

**Password**

-   New attribute `i18n` has been added.
-   New attribute `locale` has been added.

**Phone Number**

-   New attribute `i18n` has been added.
-   New attribute `locale` has been added.

**Quantity**

-   Increment and decrement buttons are no longer focusable, allowing keyboard navigation to focus solely on the input field for consistent vocalization.

**Select**

-   Items `customData` now accept an optional generic type.

**Timepicker**

-   New attribute `i18n` has been added.
-   New attribute `locale` has been added.

**Toggle**

-   New component `ToggleControl` has been added.
-   New component `ToggleLabel` has been added.
-   [DEPRECATED] Using `<Toggle />` alone will still works as expected, though we advise to move to composition using `ToggleControl` and `ToggleLabel`.

### Bug fixes:

**Combobox**

-   Click on `FormField` label now behave like native.

**File Upload**

-   Click on `FormField` label now behave like native.

**Phone Number**

-   Following country codes `bq`, `gf`, `gp`, `io`, `pm`, `re`, `sj`, `sx` have been added.
-   Click on `FormField` label now focus input instead of country select.

**Popover**

-   Positioning is now correct if a custom ID is set on the trigger element.
-   The trigger is now focusable by default.

**Tooltip**

-   Positioning is now correct if a custom ID is set on the trigger element.
-   The trigger is now focusable by default.

### Documentation improvements:

-   New `Accessibility` page has been added in the `Guides` section.
-   New section `Accessibility` has been added on every component `Documentation` page with best practices and examples.
-   A `react-router` `Link` example has been added.
-   Github links on component `Documentation` pages have been fixed.
-   Atomic design link on component `Documentation` pages have been removed, as it was not relevant.
-   New link to previous major version doc has been added on component `Documentation` pages.
-   A link to form guide has been added on each `FormField` examples.
-   FAQ has been updated.

### Miscellaneous:

-   Fix Windows issues when cloning the repository and running the Storybook scripts.

## v19.0.1

---

### Feature updates:

**Pagination**

-   Add `onPageSizeChange` handler.
-   Add `pageSize` to `PaginationPageChangeDetail` detail object.

## v19.0.0

---

Version 19 of ODS is not the direct follow-up of previous versions, it is a new library built 100% on top of [React](https://react.dev) .

### Project changes:

Previous versions were providing a React version of each components too, but there were in fact a wrapping of the Web Component. Thus it was designed with the constraints related to Web Component (no virtual DOM, relying on DOM selector, ...).

With this new version we got rid of those constraints, which gave us some benefits like:

-   component composition.
-   easier style customization.
-   easier React integration (no more `ref` needed, nor literal templates).

Although, we tried to stay as close as possible to previous version behavior, the way you integrate ODS in your React app should be rethought, to ensure you use components as real React elements and not wrapped Web Components anymore.

To check how to start with this new version, please follow the  documentation.

Here is a non-exhaustive list of the major changes:

-   rework of all components as React elements.
-   tree-shaking.
-   less restriction on components customization and usage.
-   reduced CSS size by moving to variable fonts.
-   no more Ods prefix on all components/constants.
-   no more web-components nor Vue wrappers provided.
-   updated icons (now with default padding embed).
-   updated various component designs (datepicker, message, modal, spinner, ...).
-   new design tokens.
-   updated documentation.
-   card default color changed.
-   ...

## v18.6.4

---

### Bug fixes:

**Clipboard**

-   Container now inherent parent width.

**Code**

-   Now correctly render large text with fixed height.

**Combobox**

-   Clicking an item now correctly select it on Safari.

**File Upload**

-   Allow same file to be uploaded again after cancellation.

**Phone Number**

-   Add following country codes: `bq`, `gf`, `gp`, `io`, `pm`, `re`, `sj`, `sx`.

## v18.6.3

---

### Bug fixes:

**Pagination**

-   Now handles very large number of pages correctly.

## v18.6.2

---

### Bug fixes:

**Combobox**

-   Ensure "Add new entry" option is visible when no items are present.
-   Hide "Add new entry" option when input is cleared.
-   Prevent duplicates when using value or default value in allow multiple mode.

## v18.6.1

---

### Bug fixes:

**Clipboard**

-   Remove spacing between actions and copy button.

**Input**

-   Add spacing between value and actions.

## v18.6.0

---

### New components:

**Combobox**

### Feature updates:

**Badge**

-   New feature to allow icon to be aligned either on left or right side.
-   New attribute `iconAlignment` has been added.
-   New constant `ODS_BADGE_ICON_ALIGNMENT` has been added.

**Clipboard**

-   New attribute `isMasked` has been added.

**Icon**

-   `alt` attribute has been removed. This optional attribute was in fact doing nothing at all, it can safely be kept if you were using it, but you should remove it and follow the  instead.

### Bug fixes:

**Button**

-   Ensure button height stays the same for all variants.
-   Fix `sm` height on all variants.

**Modal**

-   Backdrop is no longer removed when pressing `Escape` on non-dismissible modal.

### Documentation improvements:

-   Add new dedicated page about design tokens under the `Design and Style` section.
-   Update the accessibility section of the  component, with best practices and examples.

## v18.5.3

---

### Feature updates:

**Select**

-   New attribute `strategy` added that let you manage different positioning, default strategy is `absolute`, you can change to `fixed` when in a fixed context (like in an ods-modal for example).
-   New constants `ODS_SELECT_STRATEGY` and `ODS_SELECT_STRATEGIES` are now available.

## v18.5.2

---

### Feature updates:

**Code**

-   Add tooltip message on copy button.
-   New attribute `labelCopy` added.
-   New attribute `labelCopySuccess` added.

**Design Tokens**

-   Add new design token `--ods-form-element-input-height`. Useful when trying to use any input (ods-input, ods-datepicker, ods-select, ...) default height.

**Modal**

-   New part `dialog-content` added that let you customize the content of the modal (ie, between header and actions).

**Phone Number**

-   Pressing a letter while focusing the country selector will now move to the first matching country.

### Bug fixes:

**Code**

-   Spacing will now be correctly preserved.

**Input**

-   Fix disabled style.

**Phone Number**

-   Countries are now correctly sorted when locale get changed.

**Spinner**

-   Spinner now use inline svg instead of svg file. This should remove some of the `eval` warnings on build.

**Tabs**

-   Tabs bottom border now takes the whole width on horizontal overflow context.

### Documentation improvements:

-   Add more documentation about managing overlays on a modal.

## v18.5.1

---

### Feature updates:

**Clipboard**

-   New `ariaLabel` prop has been added.
-   New `ariaLabelledby` prop has been added.
-   New `name` prop has been added.

**Datepicker**

-   New attribute `strategy` added that let you manage different positioning, default strategy is `absolute`, you can change to `fixed` when in a fixed context (like in an ods-modal for example).
-   New constants `ODS_DATEPICKER_STRATEGY` and `ODS_DATEPICKER_STRATEGIES` are now available.

**Medium**

-   Now expose an `image` part.

**Password**

-   New `maxlength` prop has been added.
-   New `minlength` prop has been added.

**Textarea**

-   New `maxlength` prop has been added.
-   New `minlength` prop has been added.

### Bug fixes:

**Accordion**

-   Clickable elements in the slots now behaves as expected when using the React component.

**Checkbox**

-   Fix focus style on error state.

**Code**

-   Copy button is now correctly positioned on the right side.

**Datepicker**

-   Clear button is now correctly displayed when `isClearable` is set, even for invalid value.

**Input**

-   Clear button is now correctly displayed when `isClearable` is set, even for invalid value.

**Password**

-   Clear button is now correctly displayed when `isClearable` is set, even for invalid value.

**Phone Number**

-   Reset now correctly reset the flag, when `countries` is set.
-   Clear button is now correctly displayed when `isClearable` is set, even for invalid value.

**Popover**

-   `triggerId` & `shadowDomTriggerId` now accepts id with special character (ex: `?`, `:`, ...).

**Radio**

-   Fix focus style on error state.

**Tooltip**

-   `triggerId` & `shadowDomTriggerId` now accepts id with special character (ex: `?`, `:`, ...).

### Documentation improvements:

-   Headings link url are now static, which means you can safely bookmark / share them.
-   Fix the `Open canvas in new tab` action in the toolbar on Demo pages.
-   Accessibility tabs has been removed from Demo pages, as it was giving wrong information (as demo are custom made and not valid use case).

## v18.5.0

---

### New components:

**Drawer**

### Feature updates:

**Accordion**

-   Update internal logic to use the native behavior of `details` / `summary` tags.
-   Interactive elements can now be used inside the accordion without toggling it.
-   New methods `close`, `open` and `toggle` have been added.
-   Add some documentation about usage with React state.

**Link**

-   Link will now display an ellipsis by default.
-   New part `label` has been added to allow label customization.

**Modal**

-   Update default width and height.
-   Update dimensions on mobile-size screen.
-   `z-index` has been updated to use ODS overlay z indexes ordering.

**Range**

-   Range can now display ticks.
-   New attribute `ticks` has been added.
-   Thumb hitbox has been improved.

**Select**

-   `z-index` has been updated to use ODS overlay z indexes ordering.

**Tooltip**

-   Tooltip now stays open for a short time on trigger leave, which allows user to move over it and keep it open (and for example select its content).

### Documentation improvements:

-   Add new guide about using components events.
-   Add new guide about using components methods.
-   Update Properties array in the technical page to more clearly display which props are required.
-   Update form-field caption examples.
-   Fix FAQ broken link.

## v18.4.1

---

### Feature updates:

**Tabs**

-   Apply new style on unselected tab text.
-   Apply new hovering style.

**Toggle**

-   Expose new method `toggle`.

### Bug fixes:

**Toggle**

-   Fix toggle style when selected and in an error state.

### Miscellaneous:

We did integrate [react-testing-library](https://testing-library.com/docs/react-testing-library/intro) to our React wrapper test suites, so that we can replicate more easily issues integrators may encounter.

## v18.4.0

---

### Changes on form elements:

This version is focused on implementing [ValidityState](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) to our form elements.

This allows you to setup a fully working form without the need of any external library or custom validation code, as everything behave as a native form.

Though you can of course use a lib on top of that if you want to. We've validated forms using native code (vanilla and React), [Formik](https://formik.org) and [React Hook Form](https://react-hook-form.com) on our tests and playgrounds.

The changes on each component are massive, but we did focus on ensuring bumping to this version would be transparent for current ODS users.

We won't go through an exhaustive list of changes, here are the most important ones for each form element:

-   Consistent behavior regarding blur and error state.
-   Consistent event triggering (check the guide about event lifecycle).
-   New event `odsInvalid` triggered when component internal validity change.
-   New methods available: `checkValidity`, `getValidationMessage`, `getValidity`, `reportValidity`, `willValidate`.
-   New interface `OdsFormElement` available with this methods.
-   Tons of unit & e2e tests have been added.

### Feature updates:

**Checkbox**

-   New `hasError` prop has been added.
-   New style when on error.

**Clipboard**

-   Now expose an `input` part.
-   Input now inherit component length.

**Datepicker**

-   `defaultValue` now accepts Date or string.
-   `max` now accepts Date or string.
-   `min` now accepts Date or string.
-   Added a new helper method `formatDate`.
-   Added a new helper method `parseDate`.

**Input**

-   New input type `search` has been added.

**Phone Number**

-   New `customValidityMessage` prop has been added.

**Radio**

-   New `hasError` prop has been added.
-   New style when on error.

**Range**

-   Now triggers an odsChange on thumb release, not on every move.

**Select**

-   Add a new method `updateCustomRenderer` if you need to update the custom template used.

**Switch**

-   Events are now send from `ods-switch` and not from `ods-switch-item` anymore. You may need to move your handlers if you were defining some on items.
-   New `hasError` prop to enforce error state.
-   Focus style has been updated.
-   New style when on error.

**Toggle**

-   New `hasError` prop to enforce error state.
-   New style when on error.

### Documentation improvements:

-   New Guide section, with a first one about form management.
-   Added new control to visualize ValidityState on each form elements Demo page.
-   Storybook update popup has been disabled.

## v18.3.1

---

### Feature updates:

**Icon**

-   New icon "columns" has been added.

**Link**

-   Update hover & visited color to a lighter one.

**Select**

-   Update hover style to differentiate it from selected option.

**Text**

-   `caption` preset does not use a `caption` tag anymore, as these is tight to table usage only.
-   New examples have been added regarding `caption` and `figcaption` usage.

### Bug fixes:

**Breadcrumb**

-   Separator is now always correctly aligned, regardless of the number of items.

**Modal**

-   Dialog content height is now correct on Safari

## v18.3.0

---

### Feature updates:

**All form elements**

-   Apply new style on readonly state.

**Breadcrumb**

-   A new part `last` is now available to customize the last item (which differ from other link items).

**Input**

-   You can now link a native `datalist` to an `ods-input` using the `list` attribute and the `list` slot. A full example is available in the  page.
-   New attribute `list` has been added.
-   New slot `list` is now available.

**Message**

-   Message can now be rendered without the close button, as a static message.
-   New attribute `isDismissible` has been added.

**Popover**

-   New attribute `strategy` added that let you manage different positioning, default strategy is `absolute`, you can change to `fixed` when in a fixed context (like in an ods-modal for example).
-   New constants `ODS_POPOVER_STRATEGY` and `ODS_POPOVER_STRATEGIES` are now available.

**Tooltip**

-   New attribute `strategy` added that let you manage different positioning, default strategy is `absolute`, you can change to `fixed` when in a fixed context (like in an ods-modal for example).
-   New constants `ODS_TOOLTIP_STRATEGY` and `ODS_TOOLTIP_STRATEGIES` are now available.

### Bug fixes:

**All form elements**

-   Pressing `Enter` on an action button (like the clear button) does not trigger a form submit anymore.

**Badge**

-   Now use the expected border-radius style.

**Breadcrumb**

-   Last attribute is now correctly reset on items changes.

**Modal**

-   `odsClose` event is no more emitted on component unmount. If you were relying on that, please reach out to us so we can find a better approach.
-   Content is now wrapped by a div to prevent flex weird rendering.
-   Modal height can now be customized using the `dialog` part.

**Range**

-   Apply correct style to hovered thumb on error state.

**Select**

-   Options are now correctly updated on change.
-   Disable state is now correctly updated on change.

### Internal changes:

We now use an `ods-button` instead of a native one for all our internal actions. Ex: input clear, message close, modal close, ... This shouldn't have any impacts.

## v18.2.0

---

### Feature updates:

**Button**

-   New color style `neutral` and new size `xs`are now available.
-   New color `neutral` added to `ODS_BUTTON_COLOR`.
-   New size `xs` added to `ODS_BUTTON_SIZE`.

**Spinner**

-   New size `xs` is now available.
-   New size `xs` added to `ODS_SPINNER_SIZE`.

### Bug fixes:

**Modal**

-   No `odsClose` event emitted anymore on element DOM disconnection if modal was already closed.

**Password / Phone Number / Quantity / Timepicker**

-   Setting a `width: 100%` to the ods element now correctly apply it to the underneath elements.

## v18.1.0

---

### Documentation improvements:

Storybook has been partially rewritten, so you may notice a few changes:

-   some style has been updated
-   broken links have been fixed
-   components documentation has been migrated from `typedoc` to `custom-element-manifest`

### Feature updates:

**Button**

-   New feature to allow icon to be aligned either on left or right side.
-   New attribute `iconAlignment` has been added.
-   New constant `ODS_BUTTON_ICON_ALIGNMENT` has been added.

**Icon**

-   New icons `bill` and `box` have been added.
-   `globe` icon has been updated.

**Link**

-   New feature to allow icon to be aligned either on left or right side.
-   New attribute `iconAlignment` has been added.
-   New constant `ODS_LINK_ICON_ALIGNMENT` has been added.

**Select**

-   Apply new style to multi-select counter.

**Table**

-   Update header color to neutral-050.

**Text**

-   No more default margin on any presets.

### Bug fixes:

**All**

-   Components now correctly inherits font-family.

**Modal**

-   Prevent undefined element when using with react testing library.

**Phone Number**

-   Locale fallback has been moved from french to english (fallback that is applied if no locale or a wrong locale is set and the locale of your browser is not managed).

## v18.0.0

---

As it is a major version, you'll have to follow the 

### Project changes:

From the beginning of the project to the latest v17 version, our vision of the project has greatly evolved. Thanks to real-user feedbacks, a better understanding of our use cases and how ODS fits in the OVHCloud ecosystem.

With this new version we've tried to tackle most of the known issues to offer a better end-user experience:

-   components are now behaving more like native html elements
-   components are now easier to use and to customize
-   no more invasive `console.log`, we only print meaningful warnings when needed
-   enum values can now be used directly instead of importing it
-   events are now normalized for all components
-   release process will be shorter

On top of that, the v18 does bring new features:

-   new components: file-upload and timepicker
-   latest design applied on all components
-   now sync with figma changes
-   a new set of icons
-   a reworked documentation

To achieve all this, we had to rewrite almost the whole library.

This is a major version change with tons of breaking changes. But this aims to be the last time.

To help you moving from a previous version to the v18, we've added a dedicated page on each component presenting the changes and some migration examples.

### ODS libraries

The following libs have been removed:

-   @ovhcloud/ods-cdk
-   @ovhcloud/ods-common-core
-   @ovhcloud/ods-common-stencil
-   @ovhcloud/ods-common-testing
-   @ovhcloud/ods-common-theming
-   @ovhcloud/ods-theme-blue-jeans

There is now only two library published that you should include:

-   `@ovhcloud/ods-components` that contain all web-components, react and vue wrappers and the sass mixins
-   `@ovhcloud/ods-themes` that contain the design tokens and the assets (fonts and icons)

The CDK has been removed and we do not plan on publishing it again. If you were using part of it, please reach out to us so that we can get more information about your usage.

All common libs have been and we do not plan on publishing them again. If you were using part of them, please reach out to us so that we can get more information about your usage.

The `blue-jeans` theme has been replaced by the default theme of `@ovhcloud/ods-themes`.

### ODS dependencies upgrade

The new version comes with the following upgrade on major library:

-   Stencil bumps from `4.12.0` to `4.16.0`
-   `node-sass` is not supported anymore and was replaced by `sass` (`1.71.0`)
-   Typescript bumps from `4.7.4` to `5.3.3`
-   Storybook bumps from `6.4.19` to `8.0.4`

### Style customization

Although the ODS component comes with the expected design, you can now apply your own style directly on some part of the web-component shadow DOM, thanks to the [part selector](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) .

For example, you can add style on the native `input` element of an `ods-input` component through the following code:

```html
<style>
  .my-input::part(input) {
    width: 300px;
  }
</style>
<ods-input class="my-input" ... />
```

Most of the component exposes one or more part like this, you can find more information on each component documentation page.

### Sass helpers

In addition to the easier customization, we're also exposing the "basic" style of some core component as Sass mixins that you can use to apply on a native element to get the same ODS rendering.

For example, if you need to make an anchor tag looks like an `ods-link`:

```css
@import '@ovhcloud/ods-components/style';
.my-link {
  @include ods-link();
  @include ods-link-color('primary');
}
```

```html
<a class="my-link" ...>...</a>
```

This can be useful when you can't manage some part of the DOM as you want, for example using a library that will always render an anchor and that provide no custom templating.

Be careful though, this does only provide basic style, not all the feature of an ODS component, so we do recommend to use the web-component by default.

### New components:

**File Upload**

**Timepicker**

### Component changes:

Please check the  regarding all the component changes.

## v17.2.2

---

This update brings a few bug fixes across components, ensuring a smoother and more accessible user experience.

### Bug fixes:

**Button**

-   Fix the issue where the button was still clickable on the border while disabled.

### Feature updates:

**Radio-button**

-   Add 'XS' size to radio button.

## v17.2.1

---

This update brings a few bug fixes across components, ensuring a smoother and more accessible user experience.

### Bug fixes:

**Clipboard**

-   A white background is now properly displayed on the `Popover`.

**Menu**

-   A minimal height is now forced in the `Menu` component, giving proper styling on the bottom margin.

## v17.2.0

---

This update introduces our new `Table` component, enhances various features and improves documentation. It also brings numerous bug fixes across components, ensuring a smoother and more accessible user experience.

### New component:

**Table**

### Documentation improvements:

**Form Field**

-   Added an accessibility label.
-   Updated Storybook Usage Guidelines.

### Feature updates:

**Clipboard**

-   `Tooltip` now features a `3 seconds` closing delay.
-   `Tooltip` won't be displayed if `success-message` OR `error-message` is not defined.

**Datepicker**

-   `Datepicker` days are now displayed with 2 letters.

**Pagination**

-   New attribute `defaultCurrentPage` has been added.
-   New method `getCurrentPage()` has been added.
-   Old attribute `current` has been removed.
-   Navigation will be hidden if `totalItems` is lower than `itemPerPage`

### Bug fixes:

**Breadcrumb** **Link**

-   Multiple active states issue on Safari browsers has been fixed.

**Datepicker**

-   Error color on `Datepicker`'s border has been fixed.

**Input**

-   Clearing the `Input` now properly focuses it afterwards.

**Modal**

-   Overlaying elements can now overflow from the `Modal`.
-   Pressing the `ESCAPE` key when a `Select` is open inside a `Modal` no longer displays an unwanted backdrop.
-   Width value has been fixed on breakpoint.

**Popover**

-   Overlaying elements can now overflow from the `Popover`.

**Textarea**

-   `Textarea` style has been fixed to match the `Input`.

**Tile**

-   Checked `Tile` styling has been fixed on Safari.

**Search Bar**

-   `odsSearchSubmit` event is now properly triggered on `KEY_DOWN` input.

## v17.1.0

---

As we're working towards a massive uniformization of all ODS form elements (events, nav behavior, required state, validity state, ...), the amount of changes is too important to be released immediately.

This release brings the expected form-association of form elements, so that they act as native one for formData submit and reset.

It also fixes the missing types of event (`odsBlur`, `odsFocus`, `odsValueChange`).

### Documentation improvements:

Documentation usage guidelines have been completed with almost all components properties and slots:

-   tons of new components examples
-   dark background when `contrasted` prop is set
-   shortcuts have been disabled due to issue with some input preview

### Feature update:

**Pagination**

-   A new `defaultItemsPerPage` has been added, which allows to define the initial value of `itemsPerPage` on first render

### Bug fixes:

**Button / Checkbox / Radio / Range / Tile**

-   Prevent click event on click when disabled on Chromium & Safari.

**Modal**

-   Modal is now correctly cleaned up on disconnect

**Tile**

-   Ensure position stays consistent regarding end/start slot emptiness

### Miscellaneous:

-   Upgrade to StencilJS 4.12

## v17.0.3

---

### Bug fixes:

**Input**

-   Icon size are now displayed at the expected size

**Quantity**

-   Native arrow are now hidden on Safari

**Radio / Radio group**

-   Remove logger issue on documentation example page

**Toggle**

-   Toggle now shrink correctly when slot content are very large

**Tile**

-   Tile borders are now correctly rounded on Safari

## v17.0.2

---

### Bug fixes:

**Modal**

-   Added `enter` and `space` keys binding on the close button + cancel button on Storybook
-   Fixed display problems within the modal that could lead to some elements getting shrunk
-   Scroll is now prevented when a modal is open

## v17.0.1

---

### Bug fixes:

**Pagination**

-   Next & previous buttons have been fixed on Chrome & Safari where they could allow the user to go out of bound.

**Radio-Button**

-   Smaller text has been fixed and is now aligning vertically as well.

**Fonts**

-   Fonts have been fixed to display the correct font on Safari.

## v17.0.0

---

As it is a major version, you'll have to follow the 

### Project changes:

Main focus on this version has been on simplifying ODS integration in existing projects (React, Vue, JS, ...).

We now only publish one `ods-components` library that embed all the ODS components.

What this means for your code:

-   `ods-component-xxx` are no longer published and should be removed
-   only one dependency to add to your `package.json`

```text
"ods-components": "17.0.0"
```

ODS components can then be either imported using lazy-loading (the code of each component is loaded on demand) or as a module (the code of the component is tree-shaked by your own bundling system).

For more details on how to import ODS components, see:

-   the updated 
-   the 16.x to 17.x 

### Documentation improvements:

We went through each component to update:

-   the Demo controls to be more relevant
-   the Usage Guidelines, with more examples and the missing properties
-   with React example when relevant (ex: `datagrid` reactFormatter with React 18 or `tabs` with React Router)

### Component changes:

**Pagination**

-   Add `odsItemPerPageChange` when the item per page select change

### Bug fixes:

**Datagrid**

-   Overflowing elements can now correctly be displayed (see updated documentation)

**Menu**

-   Ellipsis is now correctly displayed on Safari

**Modal**

-   Inner element with overlay are now correctly displayed

**Tabs**

-   Fix hover rendering

## v16.6.0

---

### Feature update:

**Breadcrumb**

-   Add attributes on `breadcrumItem` : `disabled`, `rel`, `referrerpolicy`, `target`
-   Add event on `breadcrumItem` : `odsBreadcrumbItemClick`
-   Update `OdsHTMLAnchorElementRel` type

**Chip**

-   Added `odsChipRemoval` event
-   If `OsdsChip` is removable and is clicked, it will emit event `odsChipRemoval`
-   If `OsdsChip` is removable, focused on and key Enter is pressed, it will emit event `odsChipRemoval`

### Bug fixes:

**Button**

-   Fix position of end slot when start slot is empty in `OsdsButton`

**Datagrid**

-   If the `OsdsDatagrid` contains an overflowing on-interaction element (such as `OsdsMenu`, `OsdsTooltip`, `OsdsSelect` or `OsdsPopover`), the element is either being displayed at the top left corner of the `OsdsDatagrid` (native) or beneath the element, making it unvisible (React). This fix intends to correct this problem in both environments.

**Pagination**

-   Add `itemPerPage` in payload event `odsChange`
-   Watch `totalPages` & update it

**Phone Number**

-   Fix shadow of flag icon in the select that was cut behind overflow

**Components**

-   Some components were missing from package.json of components: `medium`, `range`, `toggle`

## v16.5.0

---

### Feature update:

**Documentation**

-   Multiple updates to clarify documentation. (Welcome page, Get started, Design tokens, Code, Contributing & libraries)
-   Removing Starter documentation, as it is deprecated for now.

### Bug fixes:

**Datagrid**

-   `OsdsDatagrid` now emits the event `onBottomScroll`.
-   `OsdsDatagrid` now supports React Formatters.
-   Row change has been fixed.

**Modal**

-   `OsdsModal` is now using a `HTMLDialogElement`,

**Phone Number**

-   Flags have been fixed to be displayed with proper width & height.

**Select**

-   Enhanced React support for `OsdsSelect`.

**Storybook**

-   Storybook errors with displaying `OsdsPhoneNumber` & `OsdsSearchBar` inputs has been fixed.

## v16.4.0

---

### New component:

**Datagrid**

-   See Documentation using version selector

### Bug fixes:

**Phone number**

-   Now consistently triggers `odsValueChange` event on type.

**Storybook**

-   The release list now correctly displays the name of the latest version.

### Feature update:

**Component Generator**

-   Component Generator has been upgraded to align with the new architecture (introduced in version 16.0.0).

## v16.3.2

---

### New component:

**Medium**

### Bug fixes:

**Phone number**

-   Emit event when the value is valid

**Accordion**

-   Can't open when the component is disabled

**Quantity**

-   Change the value onblur to be valid with step

**Modal**

-   Add event open & close (odsModalOpen, odsModalClose)

## v16.3.1

---

### Bug fixes:

**Datepicker**

-   The siblings months days are now updated after the datepicker was updated

**Select**

-   Keyboard navigation correction

## v16.3.0

---

### Component changes:

**Button**

-   Each variant now display correctly when using circle button

> ⚠️️ **Breaking change**

-   Circle button may render differently regarding the variant you're using

**Datepicker**

-   Component now close itself when value is selected
-   You can now hide adjacent month days with the attributes `showSiblingsMonthDays`

**Flag**

-   Component has been refactored internally, but this should not have any impacts

**Icon**

-   Component has been refactored internally, but this should not have any impacts

### Bug fixes:

**Menu**

-   Now correctly work when used with React

**Select**

-   On Safari the component is displayed correctly when using flex

**Textarea**

-   On Safari the component is displayed correctly when using flex

**Tile**

-   On Safari the component is displayed correctly when using flex

### Project changes:

-   The whole project is now built using Stenciljs v4.7

> ⚠️️ **Breaking change**

-   We don't generate the bundle `custom-elements-bundle` anymore

## v16.2.1

---

### Component changes:

**Datepicker**

-   New attribute `locale` to manage internationalization, accepted locales are for now:

-   French
-   English
-   German
-   Spanish
-   Italian
-   Polish
-   Portuguese
-   Dutch

**Text**

-   Component has been refactored internally, but this should not have any impacts

### Bug fixes:

**Datepicker**

-   Add missing constants and types exports

**Modal**

-   Is now correctly destroyed on page change

**Popover**

-   Now correctly work when used with React

**Select**

-   Selected value is now correctly updated on cnange (ex: translation change)

## v16.2.0

---

### New component:

**Datepicker**

### Component changes:

**Clipboard**

-   Added tooltip confirmation message on paste

### Bug fixes:

**Textarea**

-   Correctly emit `odsValueChange` event on value change.

### Documentation changes:

-   Improve typedoc documentation

## v16.1.1

---

### Documentation fixes:

-   Component specification pages now displays correct information
-   Tile sandbox now uses correct list of commands

## v16.1.0

---

### New component:

**Phone Number**

### Component changes:

**Input**

-   New attribute `prefixValue` that allow to display a fixed prefix in front of the input value.

**Progress Bar**

> ⚠️️ **Breaking change**

-   `max` and `value` attributes are now `number` and won`t accept` string` anymore.

### Bug fixes:

**Code**

-   Contrasted version have now the right color.

### Project changes:

-   The whole project is now built using node 18.

## v16.0.1

---

Fixes CI build issues when deploying Storybook.

## v16.0.0

---

As it is a major version, you'll have to follow the 

⚠️We're aware that not all the documentation is up-to-date with the latest technical changes ⚠️

⚠️We're currently working on improving the storybook but didn't want to delay the release much more ⚠️

⚠️If you have any issue migrating, reach us directly and we'll help you. ⚠️

### Project changes:

This version embed the whole new project architecture (initiated with the v15).

We now have seperations between:

-   `cdk` (@ovhcloud/ods-cdk) which provides public non component related utilities.
-   `common` libs (@ovhcloud/ods-common-[core|stencil|testing|theming]) which provides internal helpers.
-   `components` (@ovhcloud/ods-components or @ovhcloud/ods-component-`<component-name>`) which provide the agnostic components and their React, Vue wrapped versions.
-   `examples` (@ovhcloud/ods-starter-[react|react-vite|vue-vite]) which contains application examples that use some ODS components.
-   `shared-dependencies` to share common dependencies internally accross projects.
-   `storybook` (@ovhcloud/ods-storybook) which tends to centralize all the ODS documentation content.
-   `themes` (@ovhcloud/ods-theme-blue-jeans) which provides all existing themes that can be applied to ODS.

### Component changes:

**All Components**

-   `@ovhcloud/ods-stencil` package does not exists anymore.
    
-   Components are now accessible by adding either `@ovhcloud/ods-component-<component>` or `@ovhcloud/ods-components` dependency.
    
-   All `@ovhcloud/ods-stencil-<component>` libs have been replaced by `@ovhcloud/ods-component-<component>`.
    
-   All `@ovhcloud/ods-stencil-<component>-react` libs have been replaced by `@ovhcloud/ods-component-<component>-react`.
    
-   All `@ovhcloud/ods-stencil-<component>-vue` libs have been replaced by `@ovhcloud/ods-component-<component>- vue`.
    
-   Constants/Enums have been renamed to follow new naming convention:
    
-   All constants are now in UPPER_SNAKE_CASE.
    
-   All objects are now in singular form.
    
-   All lists are now in plural form.
    

Ex: `OdsSpinnerSize` is now `ODS_SPINNER_SIZE`. `OdsSpinnerSizeList` is now `ODS_SPINNER_SIZES`. `OdsSpinnerSizeUnion` has been removed.

-   Exported types follow also the same naming convention, thus most of them are now in singular form.

Ex: `OdsSpinnerAttributes` is now `OdsSpinnerAttribute`.

-   A lot of empty types who were exported previously have been removed.

Ex: `OdsSpinnerEvents` have been removed, as the component does not handle any events. `OdsSpinnerMethods` have been removed, as the component does not expose any methods.

### New component:

**Clipboard**

**Menu**

See documentation using version selector

**Modal**

**Search Bar**

See documentation using version selector

### Features changes:

**Button**

-   Add a new attribute `textAlign` which allows to choose the button text alignement between `center`, `end` and `start`.

### Bug fixes:

**Button**

-   Main slot is now correctly aligned vertically (visible when using icon).

**Flag**

-   Flags are now correctly displayed in the documentation.

**Link**

-   Outline is now correctly updated on viewport change.

## v15.0.1

---

-   Fix new packages release process.
-   Update documentation.

## v15.0.0

---

As it is a major version, you have to do the 

### Project changes:

This version is the first step towards the new ODS internal architecture. The goal of this refactoring is to simplify and improve the whole project to:

-   provide a better developer experience and thus reduce development time
-   normalize all components behavior / design / attributes
-   have better control on dependencies and lib version upgrade
-   have better control on what ODS expose (through component or CDK)
-   ...

This first step only impact two components `osds-text` and `osds-textarea`.

All other components will follow on next version.

Then common ODS lib and theming will be refactored on next version.

### Features changes:

-   **Text**
    
-   `OsdsText` is no longer part of `@ovhcloud/ods-stencil` package.
    
-   `OsdsText` is now accessible by adding either `@ovhcloud/ods-component-text` or `@ovhcloud/ods-components` dependency.
    
-   `@ovhcloud/ods-stencil-text` lib has been replaced by `@ovhcloud/ods-component-text`.
    
-   `@ovhcloud/ods-stencil-text-react` lib has been replaced by `@ovhcloud/ods-component-text-react`.
    
-   `@ovhcloud/ods-stencil-text-vue` lib has been replaced by `@ovhcloud/ods-component-text- vue`.
    
-   `OdsTextSize` has been removed from `@ovhcloud/ods-core` and has been replaced by `ODS_TEXT_SIZE` from `@ovhcloud/ods-component-text`.
    
-   `OdsTextSizeList` has been removed from `@ovhcloud/ods-core` and is replaced by `ODS_TEXT_SIZES` from `@ovhcloud/ods-component-text`.
    
-   `OdsTextSizeUnion` has been removed from `@ovhcloud/ods-core`.
    
-   `OdsTextLevel` has been removed from `@ovhcloud/ods-core` and has been replaced by `ODS_TEXT_LEVEL` from `@ovhcloud/ods-component-text`.
    
-   `OdsTextLevelList` has been removed from `@ovhcloud/ods-core` and is replaced by `ODS_TEXT_LEVELS` from `@ovhcloud/ods-component-text`.
    
-   `OdsTextLevelUnion` has been removed from `@ovhcloud/ods-core`.
    
-   `@ovhcloud/ods-component-text` exports only `OdsTextAttribute` as component interface.
    
-   **Textarea**
    
-   `OsdsTextarea` is no longer part of `@ovhcloud/ods-stencil` package.
    
-   `OsdsTextarea` is now accessible by adding either `@ovhcloud/ods-component-textarea` or `@ovhcloud/ods-components` dependency.
    
-   `@ovhcloud/ods-stencil-textarea` lib has been replaced by `@ovhcloud/ods-component-textarea`.
    
-   `@ovhcloud/ods-stencil-textarea-react` lib has been replaced by `@ovhcloud/ods-component-textarea-react`.
    
-   `@ovhcloud/ods-stencil-textarea-vue` lib has been replaced by `@ovhcloud/ods-component-textarea- vue`.
    
-   `OdsTextAreaSize` has been removed from `@ovhcloud/ods-core` and is replaced by `ODS_TEXTAREA_SIZE` from `@ovhcloud/ods-component-textarea`.
    
-   `OdsTextAreaSizeList` has been removed from `@ovhcloud/ods-core` and is replaced by `ODS_TEXTAREA_SIZES` from `@ovhcloud/ods-component-textarea`.
    
-   `OdsTextAreaSizeUnion` has been removed from `@ovhcloud/ods-core`.
    
-   `@ovhcloud/ods-component-textarea` exports only `OdsTextAttribute`, `OdsTextAreaEvent` and `OdsTextAreaValueChangeEvent` as component interfaces.
    
-   `OsdsTextarea` is now displayed as flex by default.
    
-   `OsdsTextarea` `flex` attribute has been removed.
    
-   `OsdsTextarea` `inline` attribute has been added.
    

## v14.1.0

---

### New features:

-   **CDK**
-   Manage surface position when located on the edge.
-   The surface can now be centered at the top or bottom of the trigger. See CDK Documentation using version selector.

We added the following components:

**Form Field**

**Switch**

### Features changes:

-   **Pagination**
    
-   Add tooltip to the arrows.
    
-   Can now show pages according to a numbers of items shown by page.
    

> For more information, check our documentation 

-   **Location-tile**
    
-   Component is now deprecated and won't be supported anymore. It will be deleted in a future release.
    

> For more information, check our replacement guide using version selector.

### Bug fixes:

-   **Radio**:
    
-   Radio button is now unfocusable when disabled
    
-   **Input**:
    
-   cursor is set to not-allowed when disabled
    

## v14.0.0

---

As it is a major version, you have to do the 

### New features:

We added the following components:

-   **Progress Bar**: 
-   **Popover**: 
-   **Tooltip**: 

Components version 2:

-   **Input V2**: 
-   new properties have been added
-   `loading`: it displays a Spinner
-   `clearable`: it clears the input value on click
-   `masked`: it indicates if the input value is hidden or not (to be used with password type)
-   new types have been added: `date`, `email`, `password`, `search`, `tel`, `text`, `time` and `url`
-   **Select V2**: it is now connected to our CDK and keyboard navigation has been added 

**Quantity** component accepts four digits in the input field:

**CDK** (Component Development Kit): new symmetry strategies have been added (left and right):

See CDK Documentation using version selector.

### Bug fixes:

-   **Tile**:
-   `hoverable` attribute has been added to avoid double focus
-   `interactive` attribute has been removed

> ⚠️️ **Breaking change**  
> For more information, check our migration guide

-   **Link**:
-   add missing color for active state
-   fix Link host container size to match its content
-   **Icon**: add `hoverable` attribute so component color can change when in selectable parent
-   **Checkbox Button**: aspect ratio has been added
-   **Quantity**: icons alignment has been fixed
-   **Textarea**: when component is resized and `flex` attribute is set to `true` and then, set back to `false`, Textarea width is no more blocked to 100%

## v13.0.0

---

As it is a major version, you have to do the 

### New features:

We added the following components:

-   Breadcrumb: 
-   Collapsible: See Documentation using version selector
-   Pagination: 
-   Spinner: 

Circle-shaped button has been added to Button Component (circle attribute)

### Bug fixes:

-   Button: `button' and` link`roles have been added (switching to`link` role when Button has a href property)
    
-   Chip: expected style on Chip focus has been applied
    
-   Icon: missing download icon has been added
    
-   Input: odsValueChange event won't emit on component init
    
-   Link:
    
-   all content in Link is now aligned as expected
    
-   href and target attributes are optional
    
-   Select :
    
-   check for outside click target inside its DOM
    
-   fixed width on non flex Select has been removed
    

> ⚠️️ **Breaking change**  
> For more information, check our migration guide

## v12.1.0

---

### New features:

We added the Tabs component.  

A component generator has been added. Now, you can use that tool to generate a new component easily.  

A coding Style has also been added so you can check our guidelines and how to write code for OVHcloud Design System project.  

The Contributing Documentation has been updated:

-   How to use a component in an other one
-   Theming: how to customize a style
-   some warning about pushing any sensitive content

### Bug fixes:

Range component color when in error has been fixed.

Some width issue on Button component has been fixed.  
When in flex, a Button can now display an ellipsis for text content when necessary.

## v12.0.5

---

### Bug fixes:

Button component of type submit in a form will now submit the form as expected (by click or Enter / Space keys).

Quantity component issue has been fixed regarding minus control that should be disabled when the input `min` attribute is equal to 0 and its value has reached 0 too.

## v12.0.0

---

First release on GitHub. There is no new feature in this version. It is now open source

## v11.0.0

---

Flag component has been modified; you’ll need to update as explained in the migration guide. For more details about assets, see  page. New attributes are available on this component: `lazy`, `assetPath` and `src`.

Before importing any component you now have to setup ods.

Dual range is now available for Range component; It offers two handles to set a min and max value.

To build resilient infrastructure and promote sustainable industrialization, we moved code that could be agnostic. Now, ODS components logic has been moved into controllers. If you want to contribute to ODS by developing new components, please refer to the how-to section for controllers in our Storybook.

Now, Button component has `download` and `rel` new attributes available when it is used as a link (href).

We had blur events on Quantity component to set the correct value regarding the Quantity bounds. It means when the user set manually a value that is out of bounds for Quantity, the value will be automatically reset to a correct value. When Quantity is out of bounds, the field value is now in error state.

## v10.2.0

---

A new library called `CDK` for `Component Development Kit` is available. The main objective is to make the development to be easier thanks to helpers. The first integrated feature is an overlay system that allows manipulating and displaying content on foreground of the page.

We added a new entire section for `contributions` in the storybook. Feel free to read the Contributing Documentation if you want to contribute.

We simplified our developer environment with a `global.dev.ts` file in order to make the developer tests isolated from the production build.

We optimized our CI/CD workflow for a more reactive and efficient build time and release time.

We fixed some issue about automated screenshot testing with some timeout.

## v10.1.0

---

We added the select component (version 1).

## v10.0.0

---

We updated a lot of third dependencies (stencilJs, typescript, jest, puppeteer, ...):

```text
@stencil/core ~2.6.0 to ~2.18.1
@stencil/sass ~1.4.1 to ~2.0.0
jest ~26.6.3 to ~27.4.5
@types/jest ~26.0.24 to ~27.5.2
jest-cli ~26.6.3 to ~27.4.5
jest-puppeteer ^5.0.4 to ~6.1.1
jest-puppeteer-preset ^5.0.4 to ~6.1.1
ts-jest ~26.5.6 to ~27.1.5
typescript 4.2.3 to 4.7.4
puppeteer ~5.5.0 to ~10.0.0
yarn-2.4.2 to yarn-3.2.4
node-fetch ~2.6.6 to ~2.6.7
ts-node ~9.1.1 to ~10.7.0
```

> If you are an `ODS` contributor, you may need to do some changes

We now build all of our components for `React` and `Vue` frameworks. They allow to use all the potential and features of your high-end JS framework. You can read the updated  to initialize an App with ODS and see how it works depending the framework.

Also, `ods-cart` is no more impacted by a parent container with `text-align:center`.

`ods-textarea` had an incorrect `spellcheck` property than could be equal to `spellcheck="false'` which is incorrect in HTML. Now it is in DOM only if it is `true`.

You can now disable animation on `ods-toggle` by defining a CSS value `--ods-toggle-transition-slider: none`.

Css variables `--ods-size-squish-n-*` and `--ods-size-stretch-n-*` are no more incorrect. It was previously set with only one number instead of two, accordingly with CSS3.

## v9.1.0

---

Adding 2 new components :

-   accordion: display a content that can be collapsed
-   code: display a snippet of code and copy it into the clipboard

If you already have customized themes, you have to declare the variables for this 2 new components. see more in the migration guide 9.x

## February 2022

---

The V1 **Design System** graphical charter is finalized. This charter contains :

-   Multiple color palettes (blue, gray, red, orange, green, availability & promotion extra colors)
-   A Typography system
-   All spacing options
-   Components:
-   **Buttons**
-   **Form items** (inputs, dropdowns, checkboxes, radios, radio buttons, toggle buttons, range, textarea, etc.)
-   **Chips & Badges**
-   **Cards**
-   **Cart**

## React Components

# Accordion

_An **Accordion** is a vertical header that reveals or hides an associated section of content._

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.

## Overview

---

The **Accordion** component delivers large amounts of content in a small space through progressive disclosure.

The header part gives the user a high-level overview of the content allowing them to decide which sections to read.

**Accordions** can make information processing and discovering more effective. However, it does hide content from the users, and it's important to account for a user not noticing or reading all the included content. If a user is likely to read all the content then then the usage of an **Accordion** would not be recommended as it adds an extra interaction to access the content; instead use a full scrolling page with normal headers.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Accordion</td></tr><tr><th scope="row">Also known as</th><td>Collapsible, FAQ</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=1-6634" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/accordion" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-accordion--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

Use any number of elements to visually display them as a group. Several collapsible panels can co-exist in a same interface.

Use this component for :

-   Organizing related information
-   Shortening pages and reducing scrolling when content is not crucial to read in full
-   When space is at a premium and long content cannot be displayed all at once, like on a mobile interface or in a side panel

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Accordion to progressively disclose content, especially in long or dense pages |
| - Ensure the summary/title of each Accordion clearly communicates what it contains |
| - Use Accordions to group related content (e.g., FAQ) |
| - Keep the content within each panel concise and scannable |
| - Use Accordion instead of Tab if users need to see multiple sections at once |

| ❌ Don't |
| --- |
| - Don't nest Accordions inside each other |
| - Don't use Accordion to hide critical information needed for completing a task |
| - Don't use vague or generic titles like "Section 1", they should be descriptive |
| - Don't expose a lot of content in the title |

### Best Practices in Context

1.  **Accordion**
2.  **Header**
3.  **Panel**

## Placement

---

The **Accordion** component is adjusted to the parent's width. It can vary based on the content, layout, and page design.

By default, the **Accordion** content is left-aligned in its container, as the header does.

It can be placed within main page content or inside a side container (as panels, or tiles).

## Behavior

---

An **Accordion** can be hovered, focused, clicked and disabled.

**Accordion** has two states : collapsed and expanded. It is collapsed by default.

When triggering the header part, the panel part should react as expanding or collapsing, depending on what previous state it had.

The mouse cursor acts as a clickable area on the Header part of the component.

The **Icon** helps the user to know if the **Accordion** is collapsed or expanded :

-   Collapsed : chevron points right
-   Expanded : chevron points down

The Header part of this component acts like a button, with hover, focus and active styles.

Same behavior and styling applies to both Desktop and Mobile modes.

## Navigation

---

### Focus Management

The **Accordion** header can receive keyboard focus and is part of the standard tab order.

Focus remains on the header after toggling the section.

If the **Accordion** is disabled, it does not receive focus and cannot be activated via keyboard.

### General Keyboard Shortcuts

Pressing Enter or Space on a focused **Accordion** header toggles its expansion or collapse.

Pressing Escape does not close an expanded **Accordion** section (unless additional custom behavior is implemented).

### Navigation between sections

If multiple **Accordions** exist, users can navigate between headers using the Tab key.

There is no looping between headers: pressing Tab on the last header moves focus to the next interactive element outside the Accordion.

Arrow keys (Arrow Up / Arrow Down) do not move between **Accordions** by default.

## Accessibility

---

This component complies with the [Accordion WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion) .

## React Components

# Accordion

## Overview

---

## Anatomy

---

Accordion

AccordionContent

AccordionItem

AccordionTrigger

---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.

## Accordion

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultValue

 | `string[]` | - | `undefined` | The initial value of the expanded accordion items. Use when you don't need to control the value of the accordion. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

multiple

 | `boolean` | - | `true` | Whether multiple accordion items can be expanded at the same time. |
| 

onChange

 | `(detail: AccordionChangeDetail) => void` | - | `undefined` | Callback fired when the state of expanded/collapsed accordion items changes. |
| 

value

 | `string[]` | - | `undefined` | The controlled value of the expanded accordion items. |

## AccordionContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## AccordionItem

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
disabled

 | `boolean` | - | `undefined` | Whether the accordion item is disabled. |
| 

value

 | `string` |  | `undefined` | The value of the accordion item. |

## AccordionTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
expandIconPosition

 | `EXPAND_ICON_POSITION` | - | `EXPAND_ICON_POSITION.right` | The position of the expand icon. |

## Enums

---

### EXPAND_ICON_POSITION

-   left =`"left"`
-   right =`"right"`

## Interfaces

---

### AccordionChangeDetail

-   `value: string[]`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-accordion-border-color | var(--ods-color-neutral-200) | 
 |
| --ods-accordion-border-color-hover | var(--ods-color-neutral-500) | 

 |
| --ods-accordion-border-radius | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-accordion-border-width | 1px | 

 |
| --ods-accordion-padding-horizontal | calc(var(--ods-theme-padding-horizontal) * 3) | 

 |
| --ods-accordion-padding-vertical | calc(var(--ods-theme-padding-vertical) * 3) | 

 |

## Examples

---

### Default

Lorem ipsum dolor sit amet.

```jsx
{
  globals: {
    imports: `import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Accordion>
      <AccordionItem value="0">
        <AccordionTrigger>
          Hello World!        </AccordionTrigger>
        <AccordionContent>
          Lorem ipsum dolor sit amet.        </AccordionContent>
      </AccordionItem>
    </Accordion>
}
```

### Multiple

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

  

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

```jsx
<>
  <Accordion>
    <AccordionItem value="1">
      <AccordionTrigger>
        <Text preset="heading-4">Multiple</Text>
      </AccordionTrigger>
      <AccordionContent>
        <Text preset="paragraph">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.        </Text>
      </AccordionContent>
    </AccordionItem>
    <AccordionItem value="2">
      <AccordionTrigger>
        <Text preset="heading-4">Multiple</Text>
      </AccordionTrigger>
      <AccordionContent>
        <Text preset="paragraph">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.        </Text>
      </AccordionContent>
    </AccordionItem>
  </Accordion>
  <br />
  <Accordion multiple={false}>
    <AccordionItem value="3">
      <AccordionTrigger>
        <Text preset="heading-4">Non-multiple</Text>
      </AccordionTrigger>
      <AccordionContent>
        <Text preset="paragraph">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.        </Text>
      </AccordionContent>
    </AccordionItem>
    <AccordionItem value="4">
      <AccordionTrigger>
        <Text preset="heading-4">Non-multiple</Text>
      </AccordionTrigger>
      <AccordionContent>
        <Text preset="paragraph">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.        </Text>
      </AccordionContent>
    </AccordionItem>
  </Accordion>
</>
```

### Disabled

Lorem ipsum dolor sit amet.

```jsx
<Accordion disabled>
  <AccordionItem value="0">
    <AccordionTrigger>
      Hello World!    </AccordionTrigger>
    <AccordionContent>
      Lorem ipsum dolor sit amet.    </AccordionContent>
  </AccordionItem>
</Accordion>
```

### ItemDisabled

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

```jsx
<Accordion>
  <AccordionItem
    disabled
    value="0"
  >
    <AccordionTrigger>
      Hello World!    </AccordionTrigger>
    <AccordionContent>
      Lorem ipsum dolor sit amet.    </AccordionContent>
  </AccordionItem>
  <AccordionItem value="1">
    <AccordionTrigger>
      Hello World!    </AccordionTrigger>
    <AccordionContent>
      Lorem ipsum dolor sit amet.    </AccordionContent>
  </AccordionItem>
</Accordion>
```

### Controlled

```jsx
const [value, setValue] = useState(['0']);
  return <Accordion value={value} onChange={detail => setValue(detail.value)}>
      <AccordionItem value="0">
        <AccordionTrigger>
          Hello World!        </AccordionTrigger>
        <AccordionContent>
          Lorem ipsum dolor sit amet.        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="1">
        <AccordionTrigger>
          Hello World!        </AccordionTrigger>
        <AccordionContent>
          Lorem ipsum dolor sit amet.        </AccordionContent>
      </AccordionItem>
    </Accordion>;
}
```

### Expand Icon Position

```jsx
{
  globals: {
    imports: `import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, EXPAND_ICON_POSITION } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Accordion>
        <AccordionItem value="0">
          <AccordionTrigger expandIconPosition={EXPAND_ICON_POSITION.right}>
            Icon on the right (default)          </AccordionTrigger>
          <AccordionContent>
            Lorem ipsum dolor sit amet.          </AccordionContent>
        </AccordionItem>
      </Accordion>
      <br />
      <Accordion>
        <AccordionItem value="1">
          <AccordionTrigger expandIconPosition={EXPAND_ICON_POSITION.left}>
            Icon on the left          </AccordionTrigger>
          <AccordionContent>
            Lorem ipsum dolor sit amet.          </AccordionContent>
        </AccordionItem>
      </Accordion>
    </>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Accordion

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultValue` | `` | No |  | The initial value of the expanded accordion items. Use when you don't need to control the value of the accordion. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `multiple` | `` | No | true | Whether multiple accordion items can be expanded at the same time. |
| `onChange` | `` | No |  | Callback fired when the state of expanded/collapsed accordion items changes. |
| `value` | `` | No |  | The controlled value of the expanded accordion items. |


## Subcomponents


### AccordionContent




### AccordionItem



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `disabled` | `` | No |  | Whether the accordion item is disabled. |
| `value` | `` | Yes |  | The value of the accordion item. |



### AccordionTrigger



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `expandIconPosition` | `` | No | EXPAND_ICON_POSITION.right | The position of the expand icon. |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Accordion defaultValue={['0']} style={{
    width: '100%'
  }}>
      <AccordionItem value="0">
        <AccordionTrigger>
          <Text preset="paragraph">
            Item number 1
          </Text>
        </AccordionTrigger>
        <AccordionContent>
          <Text preset="paragraph">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
            dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
            ea commodo consequat. Duis aute irure dolor in reprehenderit.
          </Text>
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="1">
        <AccordionTrigger>
          <Text preset="paragraph">
            Item number 2
          </Text>
        </AccordionTrigger>
        <AccordionContent>
          <Text preset="paragraph">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
            dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
            ea commodo consequat. Duis aute irure dolor in reprehenderit.
          </Text>
        </AccordionContent>
      </AccordionItem>
    </Accordion>
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [value, setValue] = useState(['0']);
    return <Accordion value={value} onChange={detail => setValue(detail.value)}>
        <AccordionItem value="0">
          <AccordionTrigger>
            Hello World!
          </AccordionTrigger>
          <AccordionContent>
            Lorem ipsum dolor sit amet.
          </AccordionContent>
        </AccordionItem>
        <AccordionItem value="1">
          <AccordionTrigger>
            Hello World!
          </AccordionTrigger>
          <AccordionContent>
            Lorem ipsum dolor sit amet.
          </AccordionContent>
        </AccordionItem>
      </Accordion>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Accordion>
      <AccordionItem value="0">
        <AccordionTrigger>
          Hello World!
        </AccordionTrigger>
        <AccordionContent>
          Lorem ipsum dolor sit amet.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
}
```

### Demo

```tsx
{
  render: (arg: AccordionProp) => <Accordion disabled={arg.disabled} multiple={arg.multiple}>
      <AccordionItem value="0">
        <AccordionTrigger>
          <Text preset="paragraph">
            Hello World!
          </Text>
        </AccordionTrigger>
        <AccordionContent>
          <Text preset="paragraph">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
            dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
            ea commodo consequat. Duis aute irure dolor in reprehenderit.
          </Text>
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="1">
        <AccordionTrigger>
          <Text preset="paragraph">
            Bye World!
          </Text>
        </AccordionTrigger>
        <AccordionContent>
          <Text preset="paragraph">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
            dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
            ea commodo consequat. Duis aute irure dolor in reprehenderit.
          </Text>
        </AccordionContent>
      </AccordionItem>
    </Accordion>,
  argTypes: orderControls({
    multiple: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Accordion disabled>
      <AccordionItem value="0">
        <AccordionTrigger>
          Hello World!
        </AccordionTrigger>
        <AccordionContent>
          Lorem ipsum dolor sit amet.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
}
```

### Expand Icon Position Example

```tsx
{
  globals: {
    imports: `import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, EXPAND_ICON_POSITION } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Accordion>
        <AccordionItem value="0">
          <AccordionTrigger expandIconPosition={EXPAND_ICON_POSITION.right}>
            Icon on the right (default)
          </AccordionTrigger>
          <AccordionContent>
            Lorem ipsum dolor sit amet.
          </AccordionContent>
        </AccordionItem>
      </Accordion>
      <br />
      <Accordion>
        <AccordionItem value="1">
          <AccordionTrigger expandIconPosition={EXPAND_ICON_POSITION.left}>
            Icon on the left
          </AccordionTrigger>
          <AccordionContent>
            Lorem ipsum dolor sit amet.
          </AccordionContent>
        </AccordionItem>
      </Accordion>
    </>
}
```

### Item Disabled

```tsx
{
  globals: {
    imports: `import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Accordion>
      <AccordionItem value="0" disabled>
        <AccordionTrigger>
          Hello World!
        </AccordionTrigger>
        <AccordionContent>
          Lorem ipsum dolor sit amet.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="1">
        <AccordionTrigger>
          Hello World!
        </AccordionTrigger>
        <AccordionContent>
          Lorem ipsum dolor sit amet.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
}
```

### Multiple

```tsx
{
  globals: {
    imports: `import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Text } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Accordion>
        <AccordionItem value="1">
          <AccordionTrigger>
            <Text preset="heading-4">Multiple</Text>
          </AccordionTrigger>
          <AccordionContent>
            <Text preset="paragraph">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            </Text>
          </AccordionContent>
        </AccordionItem>
        <AccordionItem value="2">
          <AccordionTrigger>
            <Text preset="heading-4">Multiple</Text>
          </AccordionTrigger>
          <AccordionContent>
            <Text preset="paragraph">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            </Text>
          </AccordionContent>
        </AccordionItem>
      </Accordion>
      <br />
      <Accordion multiple={false}>
        <AccordionItem value="3">
          <AccordionTrigger>
            <Text preset="heading-4">Non-multiple</Text>
          </AccordionTrigger>
          <AccordionContent>
            <Text preset="paragraph">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            </Text>
          </AccordionContent>
        </AccordionItem>
        <AccordionItem value="4">
          <AccordionTrigger>
            <Text preset="heading-4">Non-multiple</Text>
          </AccordionTrigger>
          <AccordionContent>
            <Text preset="paragraph">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            </Text>
          </AccordionContent>
        </AccordionItem>
      </Accordion>
    </>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Accordion>
      <AccordionItem value="0">
        <AccordionTrigger>
          <Text preset="paragraph">
            Hello World!
          </Text>
        </AccordionTrigger>
        <AccordionContent>
          <Text preset="paragraph">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
            dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
            ea commodo consequat. Duis aute irure dolor in reprehenderit.
          </Text>
        </AccordionContent>
      </AccordionItem>
    </Accordion>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  render: ({}) => <Accordion>
      <AccordionItem value="0">
        <AccordionTrigger>
          <Text preset="paragraph">Hello World!</Text>
        </AccordionTrigger>
        <AccordionContent>
          <Text preset="paragraph">Lorem ipsum dolor sit amet.</Text>
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="1" disabled>
        <AccordionTrigger>
          <Text preset="paragraph">Disabled item</Text>
        </AccordionTrigger>
        <AccordionContent>
          <Text preset="paragraph">This item is disabled.</Text>
        </AccordionContent>
      </AccordionItem>
    </Accordion>,
  tags: ['!dev']
}
```

## React Components

# Avatar

_**Avatar** is a visual representation of a user or entity, supporting multiple content types such as user images, icons, and user initials._

## Overview

---

The **Avatar** component is a visual representation of a user or entity, supporting multiple content types such as user images, icons, and user initials.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Avatar</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/mwZtfuQ9nzv6fY0dfez4NZ/-Q3-2026--IA-element?node-id=18697-768" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/avatar" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Avatar** component is commonly used to represent users, bots, or systems in various applications, such as chat interfaces, user profiles, or notification systems.

It is commonly used for:

-   **User profiles**: display a user's avatar next to their name and other profile information
-   **Chat interfaces**: use avatars to represent users or bots in a conversation
-   **Notification systems**: display an avatar for the sender or recipient of a notification
-   **Team or member lists**: show avatars for each team member or user in a list
-   **Comment sections**: display an avatar for each commenter to help identify the author of a comment

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the Avatar component to represent a user or entity in a concise and recognizable way |
| - Choose the appropriate content type (image, icon, or initials) based on the context and available information |
| - Use the Avatar component with sufficient contrast or color scheme |

| ❌ Don't |
| --- |
| - Don't use the Avatar component as a decorative element or without a clear purpose |
| - Don't overload the Avatar component with too much information or complex graphics |
| - Don't use an Avatar that is too small or too large for the context, which can affect readability and recognition |
| - Don't forget to provide a fallback logic for cases where the image fails to load or no name is provided (i.e. initials, icons) |

### Best Practices in Context

1.  **Avatar**

## Behavior

---

The **Avatar** component is a static display-only element, providing a visual representation of a user or entity without any interactive functionality.

The component follows a content priority chain:

-   If **children** are provided, they are displayed (initials, custom icon, etc.)
-   If a **src** URL is provided, an image is rendered
-   If the image fails to load (error), the **fallback** content is displayed
-   If no fallback is provided, a default user icon is shown

## Navigation

---

The **Avatar** component is non-interactive and does not receive keyboard focus when used alone.

## Accessibility

---

When used inside interactive contexts (e.g. next to a user name link or inside a button), ensure the parent element provides the necessary accessible label.

When the **Avatar** displays an image, the `alt` attribute is empty by default since the avatar is typically decorative. If the avatar conveys meaningful information, wrap it in an element with an appropriate `aria-label`.

## React Components

# Avatar

## Overview

---

## Anatomy

---

Avatar

---

## Avatar

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
fallback

 | `ReactNode` | - | `<Icon name={ ICON_NAME.user } />` | Fallback content to display when the image fails to load. Defaults to a user icon. |
| 

src

 | `string` | - | `undefined` | The image source URL. |

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-avatar-background-color | var( --ods-color-primary-500 ) | 
 |
| --ods-avatar-border-radius | 50% | 

 |
| --ods-avatar-object-fit | cover | 

 |
| --ods-avatar-size | 24px | 

 |
| --ods-avatar-text-color | var(--ods-color-neutral-000) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Avatar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Avatar />
}
```

### Initials

```jsx
{
  globals: {
    imports: `import { Avatar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Avatar>
      JD    </Avatar>
}
```

### With Image

```jsx
{
  globals: {
    imports: `import { Avatar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Avatar src="https://images.crunchbase.com/image/upload/c_pad,h_160,w_160,f_auto,b_white,q_auto:eco,dpr_1/fhi0pe8wd87fvujy9yk8" />
}
```

### With Fallback

J

```jsx
<Avatar
  fallback="J"
  src="https://invalid-url.example.com/avatar.png"
/>
```

### With Icon

```jsx
<Avatar>
  <Icon name="sparkle" />
</Avatar>
```

## Recipes

---

Chat

Assistant2:58 PM

Welcome to the Chat recipe. Feel free to test the UI behavior by typing anything on your mind below.

## React Components/Avatar

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `fallback` | `` | No | <Icon name={ ICON_NAME.user } /> | Fallback content to display when the image fails to load. Defaults to a user icon. |
| `src` | `` | No |  | The image source URL. |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Avatar />
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Avatar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Avatar />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    fallback: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    src: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    }
  })
}
```

### Initials

```tsx
{
  globals: {
    imports: `import { Avatar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Avatar>
      JD
    </Avatar>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Avatar />
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    gap: '8px',
    alignItems: 'center'
  }}>
      <Avatar />
      <Avatar>JD</Avatar>
      <Avatar>
        <Icon name={ICON_NAME.sparkle} />
      </Avatar>
    </div>
}
```

### With Fallback

```tsx
{
  globals: {
    imports: `import { Avatar, Icon, ICON_NAME } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Avatar fallback={"J"} src="https://invalid-url.example.com/avatar.png" />
}
```

### With Icon

```tsx
{
  globals: {
    imports: `import { Avatar, Icon, ICON_NAME } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Avatar>
      <Icon name={ICON_NAME.sparkle} />
    </Avatar>
}
```

### With Image

```tsx
{
  globals: {
    imports: `import { Avatar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Avatar src="https://images.crunchbase.com/image/upload/c_pad,h_160,w_160,f_auto,b_white,q_auto:eco,dpr_1/fhi0pe8wd87fvujy9yk8" />
}
```

## React Components

# Badge

_**Badge** show concise metadata or the current status for an item, in a compact format._

Badge

## Overview

---

A **Badge** component labels an item, a keyword or a status. They are non-interactive and used inline, next to the title or text they complete.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Badge</td></tr><tr><th scope="row">Also known as</th><td>Chip (previous name), Label</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=3-22285" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/badge" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-badge--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

Badges are used for items that need to be labelled or categorized.

It can refer to these examples of enhanced data :

-   A notification status
-   A marketing category
-   Additional product information

### Notification Status

The notification status can be used next to a menu entry, a label or an option ; in dashboards, Table or summary panels.

There are four different sub-types of status, in order of severity:

-   **Error** : Reserved for errors, malfunctions, as well as critical issues. System is unusable, or an action must be taken immediately.
-   **Warning** : Reserved for critical messages that need the user attention and acknowledgment, but might not cause errors.
-   **Information** : Provides information to users in context. Shouldn't replace regular content.
-   **Success** : Reserved to provide to a static persistent success message.

### Marketing category

The marketing category is used in product Tiles or product descriptions; used as - temporary or permanent - marketing highlight.

It can contain categories like _"Promotion", "Summer Deals", "Black Friday", "Limited Edition", "Flash Sales"_, ...

### Additional product information

The product status is used in product Tiles or product descriptions, it is bound inside the product structure to add tags and information.

All product statuses are of type "New", "Limited edition", "Sold out"

### Dos & Don'ts

| ✅ Do |
| --- |
| - Keep the label short, specific, and readable (ideally one or two words) |
| - Use Badge to indicate status, category, or metadata |
| - Place Badge inline with related content, such as next to titles or list items |
| - Use Badge to complement information, not replace it. They should add clarity, not create ambiguity |

| ❌ Don't |
| --- |
| - Don't use Badge as interactive element (e.g., buttons or filters) |
| - Don't write long sentences or complex phrases inside a Badge |
| - Don't use too many Badges in the same line or component, as this creates visual clutter |
| - Don't place Badge far from the content they describe, proximity reinforces meaning |

### Best Practices in Context

1.  **Badge**
2.  **Icon** - optional (left or right)
3.  **Label** - optional (specific use case only)

## Placement

---

Since it provides extra information to a sibling element, in can be used inside components in various places, referring to the nature of its environment.

Multiple **Badges** can be displayed:

-   on a single line
-   stacked vertically

## Behavior

---

As the **Badge** is an informational component, its default behavior is being read-only.

An icon can be displayed on the left or right of the **Badge** label content. Icon-only **Badge** also exists, but it must meet accessibility requirements. See Accessibility section below.

## Variation

---

### Color

-   **Information** _(default)_: display neutral or informational messages, such as updates, notifications, or general status.
-   **Success**: indicate positive outcomes or successful actions, such as completed tasks or achievements.
-   **Warning**: alert users to potential issues or cautionary information, signaling that attention is needed.

### Size

-   **Small**: compact and unobtrusive counts or statuses, suitable for tight spaces and minimalistic designs.
-   **Medium** _(default)_: main size for displaying **Badges**.
-   **Large**: prominent and easily noticeable counts or statuses, suitable for emphasizing important information.

## Navigation

---

The **Badge** component is non-interactive and does not receive keyboard focus. It is purely decorative and used to label an item, keyword, or status.

## Accessibility

---

**Badges** are read by screen readers as regular text.

Icon-only **Badge** is only used in very specific cases. In that case, it should be accompanied by a tooltip to provide further context.

The accessibility of multiple **Badges** presented as a collection must be considered during its implementation. Their grouping should be made clear to assistive technologies and proper ARIA attributes must be added to ensure full accessibility.

### Icon-only Badge

If a **Badge** contains only an icon, it must include an [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) to provide context.

```jsx
<Badge>
  <Icon
    aria-label="Promotion"
    name="tag"
    role="img"
  />
</Badge>
```

Screen readers will announce the aria-label instead of ignoring the **Badge**.

Since an icon-only **Badge** may lack detailed information, attach a tooltip and use an [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby) when extra context is needed:

```jsx
<Tooltip>
  <TooltipTrigger asChild>
    <Badge
      aria-labelledby="tooltip-a11y"
      color="promotion"
    >
      <Icon name="tag" />
    </Badge>
  </TooltipTrigger>
  <TooltipContent id="tooltip-a11y">
    Promotion valid from November 22 to 26  </TooltipContent>
</Tooltip>
```

Screen readers will announce the **Tooltip** content when focusing on the **Badge**.

### Structuring groups of Badges with lists

When displaying multiple **Badges** together, they should be wrapped within an unordered list of items (`<ul>` and `<li>`) to ensure a proper announcement by screen readers.

-   Item 1
-   Item 2

```jsx
<ul
  style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    listStyle: 'none',
    margin: 0,
    padding: 0
  }}
>
  <li>
    <Badge>
      Item 1    </Badge>
  </li>
  <li>
    <Badge>
      Item 2    </Badge>
  </li>
</ul>
```

This structure ensures that assistive technologies announce **Badges** as a list, rather than reading them as separate, unrelated announcements. Screen readers will announce the list, the number of items and the **Badges** content.

### Alternative approach with ARIA roles

When modifying the HTML structure is not possible, use [role="list"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/list_role) and [role="listitem"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/listitem_role) to mimic list semantics.

Item 1Item 2

```jsx
<div
  role="list"
  style={{
    alignItems: 'center',
    display: 'flex',
    flexFlow: 'row',
    gap: '8px'
  }}
>
  <Badge role="listitem">
    Item 1  </Badge>
  <Badge role="listitem">
    Item 2  </Badge>
</div>
```

This ensures that screen readers recognize the **Badges** as a structured list even without native `<ul>` and `<li>` elements.

## React Components

# Badge

## Overview

---

Badge

## Anatomy

---

Badge

---

Badge

## Badge

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |
| 
color

 | `BADGE_COLOR` | - | `BADGE_COLOR.information` | The color preset to use. |
| 

size

 | `BADGE_SIZE` | - | `BADGE_SIZE.md` | The size preset to use. |

## Enums

---

### BADGE_COLOR

-   alpha =`"alpha"`
-   beta =`"beta"`
-   critical =`"critical"`
-   information =`"information"`
-   neutral =`"neutral"`
-   new =`"new"`
-   primary =`"primary"`
-   promotion =`"promotion"`
-   success =`"success"`
-   warning =`"warning"`

### BADGE_SIZE

-   lg =`"lg"`
-   md =`"md"`
-   sm =`"sm"`

## Interfaces

---

### BADGE_COLOR

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-badge-background-color-alpha | var(--ods-color-alpha) | 
 |
| --ods-badge-background-color-beta | var(--ods-color-beta) | 

 |
| --ods-badge-background-color-critical | var(--ods-color-critical-100) | 

 |
| --ods-badge-background-color-information | var(--ods-color-information-100) | 

 |
| --ods-badge-background-color-neutral | var(--ods-color-neutral-100) | 

 |
| --ods-badge-background-color-new | var(--ods-color-new) | 

 |
| --ods-badge-background-color-primary | var(--ods-color-primary-500) | 

 |
| --ods-badge-background-color-promotion | var(--ods-color-promotion) | 

 |
| --ods-badge-background-color-success | var(--ods-color-success-100) | 

 |
| --ods-badge-background-color-warning | var(--ods-color-warning-100) | 

 |
| --ods-badge-border-radius | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-badge-padding-horizontal-lg | calc(var(--ods-theme-padding-horizontal) * 0.75) | 

 |
| --ods-badge-padding-horizontal-md | calc(var(--ods-theme-padding-horizontal) / 2) | 

 |
| --ods-badge-padding-horizontal-sm | calc(var(--ods-theme-padding-horizontal) / 2) | 

 |
| --ods-badge-padding-vertical-lg | calc(var(--ods-theme-padding-vertical) * 0.75) | 

 |
| --ods-badge-padding-vertical-md | calc(var(--ods-theme-padding-vertical) / 2) | 

 |
| --ods-badge-padding-vertical-sm | calc(var(--ods-theme-padding-vertical) / 2) | 

 |
| --ods-badge-text-color-alpha | var(--ods-color-primary-900) | 

 |
| --ods-badge-text-color-beta | var(--ods-color-primary-900) | 

 |
| --ods-badge-text-color-critical | var(--ods-color-critical-900) | 

 |
| --ods-badge-text-color-information | var(--ods-color-information-900) | 

 |
| --ods-badge-text-color-neutral | var(--ods-color-neutral-700) | 

 |
| --ods-badge-text-color-new | var(--ods-color-primary-900) | 

 |
| --ods-badge-text-color-primary | var(--ods-color-neutral-000) | 

 |
| --ods-badge-text-color-promotion | var(--ods-color-primary-800) | 

 |
| --ods-badge-text-color-success | var(--ods-color-success-900) | 

 |
| --ods-badge-text-color-warning | var(--ods-color-warning-900) | 

 |

## Examples

---

### Default

My badge

```jsx
{
  globals: {
    imports: `import { Badge } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Badge>
      My badge    </Badge>
}
```

### Color

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { BADGE_COLOR, Badge } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Badge color={BADGE_COLOR.alpha}>Alpha</Badge>
      <Badge color={BADGE_COLOR.beta}>Beta</Badge>
      <Badge color={BADGE_COLOR.critical}>Critical</Badge>
      <Badge color={BADGE_COLOR.information}>Information</Badge>
      <Badge color={BADGE_COLOR.neutral}>Neutral</Badge>
      <Badge color={BADGE_COLOR.new}>New</Badge>
      <Badge color={BADGE_COLOR.primary}>Primary</Badge>
      <Badge color={BADGE_COLOR.promotion}>Promotion</Badge>
      <Badge color={BADGE_COLOR.success}>Success</Badge>
      <Badge color={BADGE_COLOR.warning}>Warning</Badge>
    </>
}
```

### Size

SM badgeMD badgeLG badge

```jsx
<>
  <Badge size="sm">
    SM badge  </Badge>
  <Badge size="md">
    MD badge  </Badge>
  <Badge size="lg">
    LG badge  </Badge>
</>
```

## Recipes

---

Config Tile

VPS 1

4 vCore8 Go RAM100 Go 1 day automated backup Unlimited traffic 200 Mbps

12 months6 monthsNo commitment

From€24.46ex. VAT/monthor €13.19 incl. VAT/month

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Feature List Product Card

WEB HOSTING

NewBest seller

Performance

For demanding online stores and projects.

1 vCore 2,4 GHz, 2 Go RAM1 vCore 2,4 GHz, 4 Go RAM2 vCores 2,4 GHz, 8 Go RAM

From

€24.46ex. VAT/month

or €13.19 incl. VAT/monthfor a 24-month registration

Minimum 2-year registration €100 free with a 5-year registration

Installation fee:Free

-   -   Unlimited websites
        
    -   High power level
        
    -   1 domain name free for the first
        
    -   500 GB SSD storage
        
    -   1,000 email addresses
        
-   1-click CMS
    
    -   WordPress
        
    -   Joomla!
        
    -   Drupal
        
    -   Prestashop
        
-   Database
    
    -   4 x 1 GB databases
        
    -   8 GB Web Cloud Databases
        
-   Security
    
    -   Unlimited free SSL
        
    -   Anti-DDoS protection
        
    -   Anti-virus and anti-spam
        
    -   Daily backups
        
-   Performance
    
    -   99.9% observed availability
        
    -   Guaranteed resources
        
    -   Unlimited traffic
        
    -   Service continuity
        
    -   Boost option to withstand temporary traffic spikes
        
-   Support and additional services
    
    -   Git
        
    -   Standard support
        
    -   SSH access
        
    -   CDN Basic

## React Components/Badge

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No | BADGE_COLOR.information | The color preset to use. |
| `size` | `` | No | BADGE_SIZE.md | The size preset to use. |


## Examples


### Accessibility Alternative Grouping

```tsx
{
  globals: {
    imports: `import { Badge } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <div role="list" style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>
      <Badge role="listitem">
        Item 1
      </Badge>
      <Badge role="listitem">
        Item 2
      </Badge>
    </div>
}
```

### Accessibility Aria Label

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Badge, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Badge>
      <Icon aria-label="Promotion" name={ICON_NAME.tag} role="img" />
    </Badge>
}
```

### Accessibility Grouping

```tsx
{
  globals: {
    imports: `import { Badge } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ul style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    margin: 0,
    padding: 0,
    listStyle: 'none'
  }}>
      <li>
        <Badge>
          Item 1
        </Badge>
      </li>
      <li>
        <Badge>
          Item 2
        </Badge>
      </li>
    </ul>
}
```

### Accessibility With Tooltip

```tsx
{
  globals: {
    imports: `import { BADGE_COLOR, ICON_NAME, Badge, Icon, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tooltip>
      <TooltipTrigger asChild>
        <Badge aria-labelledby="tooltip-a11y" color={BADGE_COLOR.promotion}>
          <Icon name={ICON_NAME.tag} />
        </Badge>
      </TooltipTrigger>

      <TooltipContent id="tooltip-a11y">
        Promotion valid from November 22 to 26
      </TooltipContent>
    </Tooltip>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Badge>
      Badge
    </Badge>
}
```

### Color

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { BADGE_COLOR, Badge } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Badge color={BADGE_COLOR.alpha}>Alpha</Badge>
      <Badge color={BADGE_COLOR.beta}>Beta</Badge>
      <Badge color={BADGE_COLOR.critical}>Critical</Badge>
      <Badge color={BADGE_COLOR.information}>Information</Badge>
      <Badge color={BADGE_COLOR.neutral}>Neutral</Badge>
      <Badge color={BADGE_COLOR.new}>New</Badge>
      <Badge color={BADGE_COLOR.primary}>Primary</Badge>
      <Badge color={BADGE_COLOR.promotion}>Promotion</Badge>
      <Badge color={BADGE_COLOR.success}>Success</Badge>
      <Badge color={BADGE_COLOR.warning}>Warning</Badge>
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Badge } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Badge>
      My badge
    </Badge>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    children: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    color: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'BADGE_COLOR'
        }
      },
      control: {
        type: 'select'
      },
      options: BADGE_COLORS
    },
    size: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'BADGE_SIZE'
        }
      },
      control: {
        type: 'select'
      },
      options: BADGE_SIZES
    }
  }),
  args: {
    children: 'My badge'
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Badge>
      Badge
    </Badge>
}
```

### Size

```tsx
{
  globals: {
    imports: `import { BADGE_SIZE, Badge } from '@ovhcloud/ods-react';`
  },
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  tags: ['!dev'],
  render: ({}) => <>
      <Badge size={BADGE_SIZE.sm}>SM badge</Badge>
      <Badge size={BADGE_SIZE.md}>MD badge</Badge>
      <Badge size={BADGE_SIZE.lg}>LG badge</Badge>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '8px'
  }}>
      {BADGE_SIZES.map(size => <div key={size} style={{
      display: 'flex',
      flexFlow: 'row',
      flexWrap: 'wrap',
      gap: '8px',
      alignItems: 'center'
    }}>
          {BADGE_COLORS.map(color => <Badge key={`${String(size)}-${String(color)}`} size={size} color={color}>
              {String(color)}
            </Badge>)}
        </div>)}
    </div>
}
```

## React Components

# Breadcrumb

_A list of  showing the current page location in the navigational hierarchy._

1.  ---
    
2.  ---
    
3.  Current

## Overview

---

**Breadcrumb** is used to define the user's position in a site hierarchy, it can also be useful for a finer navigation in an inner application.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Breadcrumb</td></tr><tr><th scope="row">Also known as</th><td>Breadcrumb trail</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=3-22762" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/breadcrumb" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-breadcrumb--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

It has several usages :

-   Displaying subpages of a site structure
-   Show step progress of a process
-   Simplify site structure navigation in a quicker way

### Dos & Don'ts

| ✅ Do |
| --- |
| - Start the Breadcrumb trail with a link to the homepage, using a "home" Icon or the "Home" label for example |
| - Reflect the actual page hierarchy in the Breadcrumb structure (e.g., Section > Subsection > Current page) |
| - Use Breadcrumb to provide contextual orientation, especially in deep navigation paths |
| - Ensure that each Breadcrumb item (except the last one) is a clickable link to a parent page |
| - Add the ellipsis to collapse long paths (more than 4 Links visible) |

| ❌ Don't |
| --- |
| - Don't use Breadcrumbs as a primary navigation method, they are secondary and contextual only |
| - Don't include external links in Breadcrumb items, all links should be internal |
| - Don't nest Breadcrumbs or place more than one Breadcrumb component on the same page |
| - Don't exceed 4 levels of depth. Overly complex paths reduce clarity and usability |
| - Don't make the current page a link. The last item should be plain text to indicate the user's location |
| - Don't change the Breadcrumb order to reflect user history or actions, it should always match site structure |
| - Don't use Breadcrumb if the page doesn't belong to a clearly hierarchical structure |

### Best Practices in Context

1.  **Breadcrumb**
2.  **Page link**
3.  **Current page**
4.  **Ellipsis**

## Placement

---

A **Breadcrumb** is used at the top of a web page, preferably start-aligned.

Its width is automatic, relative to its content and is not adjustable.

## Behavior

---

When the **Breadcrumb** has more than 4 links visible, an ellipsis is displayed as a replacement for the middle link.

A click on the ellipsis will expand all previously hidden links inline, the collapsed state can't be redone afterward.

The **Breadcrumb** links are kept inline, even on mobile viewports.

## Navigation

---

### Focus Management

All links within the **Breadcrumb** component are focusable, following the reading order. The last item, which represents the current page, is static text and does not receive focus.

### General Keyboard Shortcuts

-   Pressing Tab moves focus through each link in the **Breadcrumb** trail
-   Pressing Enter while a link is focused navigates to the corresponding page
-   When an ellipsis (...) appears due to a long **Breadcrumb** trail (more than 4 elements), it behaves as a link:
    -   Pressing Enter expands the full **Breadcrumb** list
    -   Once expanded, additional link become accessible via Tab navigation

## Accessibility

---

This component complies with the [Breadcrumb WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/) .

### Identifying the Breadcrumb with aria-label

**Breadcrumbs** are a form of navigation, but they serve a specific purpose distinct from primary navigation menus. To ensure they are correctly recognized, an [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) should be added to explicitly identify them.

**Breadcrumb Links** follow the same accessibility guidelines as a standard .

1.  ---
    
2.  ---
    
3.  ---
    
4.  Current page

```jsx
<Breadcrumb aria-label="Breadcrumb">
  <BreadcrumbItem>
    <BreadcrumbLink
      aria-label="Home"
      href="#"
    >
      <Icon name="home" />
    </BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      Category    </BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      Subcategory    </BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      Current page    </BreadcrumbLink>
  </BreadcrumbItem>
</Breadcrumb>
```

Screen readers will announce the link, the label of the first link, the list and the number of item.

## React Components

# Breadcrumb

## Overview

---

## Anatomy

---

Breadcrumb

BreadcrumbItem

BreadcrumbLink

---

## Breadcrumb

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [nav attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/nav#attributes) . |
| 
collapseThreshold

 | `number` | - | `4` | The number of items when the component will collapse to an ellipsis. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

id

 | `string` | - | `undefined` | Id for the breadcrumb nav (overrides auto-generated id) |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

nbItemsAfterEllipsis

 | `number` | - | `1` | The number of items to display before the ellipsis. |
| 

nbItemsBeforeEllipsis

 | `number` | - | `1` | The number of items to display after the ellipsis. |
| 

noCollapse

 | `boolean` | - | `false` | Whether the component should not collapse in an ellipsis regarding the number of items. |
| 

onExpand

 | `() => void` | - | `undefined` | Callback fired when an ellipsis is expanded. |

## BreadcrumbItem

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [li attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/li#attributes) . |

## BreadcrumbLink

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [a attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#attributes) . |
| 
as

 | `<T>` | - | `'a'` | Pass a component you may want to use as custom Link component. Useful for example when using routing library like react-router. |

## Enums

---

### BREADCRUMB_I18N

-   ellipsisButton =`"breadcrumb.ellipsis.button"`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-link-column-gap | var(--ods-theme-column-gap) | 
 |
| --ods-link-row-gap | var(--ods-theme-row-gap) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb>
      <BreadcrumbItem>
        <BreadcrumbLink aria-label="Home" href="#">
          <Icon name={ICON_NAME.home} />
        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Parent        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Current        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

### Collapsed

```jsx
{
  globals: {
    imports: `import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb>
      <BreadcrumbItem>
        <BreadcrumbLink aria-label="Homepage" href="#">
          Home        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Products        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Hosting        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Servers        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Dedicated        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Rise        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          RISE-2        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

You can customize the number of item displayed before and after the ellipsis.

```jsx
{
  globals: {
    imports: `import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb nbItemsBeforeEllipsis={1} nbItemsAfterEllipsis={4}>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Home        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Products        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Hosting        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Servers        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Dedicated        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Rise        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          RISE-2        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

You can also customize the threshold from when the breadcrumb will collapse.

1.  ---
    
2.  ---
    
3.  ---
    
4.  ---
    
5.  ---
    
6.  ---
    
7.  RISE-2

```jsx
<Breadcrumb collapseThreshold={7}>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      Home    </BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      Products    </BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      Hosting    </BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      Servers    </BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      Dedicated    </BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      Rise    </BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbItem>
    <BreadcrumbLink href="#">
      RISE-2    </BreadcrumbLink>
  </BreadcrumbItem>
</Breadcrumb>
```

Or you can even deactivate the whole collapse logic.

1.  ---
    
2.  ---
    
3.  ---
    
4.  ---
    
5.  ---
    
6.  ---
    
7.  RISE-2

```jsx
{
  globals: {
    imports: `import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb noCollapse>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Home        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Products        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Hosting        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Servers        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Dedicated        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Rise        </BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          RISE-2        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Breadcrumb

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `collapseThreshold` | `` | No | 4 | The number of items when the component will collapse to an ellipsis. |
| `i18n` | `` | No |  | Internal translations override. |
| `id` | `` | No |  | Id for the breadcrumb nav (overrides auto-generated id) |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `nbItemsAfterEllipsis` | `` | No | 1 | The number of items to display before the ellipsis. |
| `nbItemsBeforeEllipsis` | `` | No | 1 | The number of items to display after the ellipsis. |
| `noCollapse` | `` | No | false | Whether the component should not collapse in an ellipsis regarding the number of items. |
| `onExpand` | `` | No |  | Callback fired when an ellipsis is expanded. |


## Subcomponents


### BreadcrumbItem



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `autoFocus` | `` | No |  | @internal |
| `isLast` | `` | No |  | @internal |



### BreadcrumbLink



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `` | No |  | @default-value='a' Pass a component you may want to use as custom Link component. Useful for example when using routing library like react-router. |
| `autoFocus` | `` | No |  | @internal |
| `isLast` | `` | No |  | @internal |


## Examples


### Accessibility With Aria Label

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Breadcrumb aria-label="Breadcrumb">
      <BreadcrumbItem>
        <BreadcrumbLink aria-label="Home" href="#">
          <Icon name={ICON_NAME.home} />
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Category
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Subcategory
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Current page
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Breadcrumb>
      <BreadcrumbItem>
        <BreadcrumbLink aria-label="Home" href="#">
          <Icon name={ICON_NAME.home} />
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Parent
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Current
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

### Collapsed

```tsx
{
  globals: {
    imports: `import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb>
      <BreadcrumbItem>
        <BreadcrumbLink aria-label="Homepage" href="#">
          Home
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Products
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Hosting
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Servers
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Dedicated
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Rise
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          RISE-2
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

### Custom After Before Collapse

```tsx
{
  globals: {
    imports: `import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb nbItemsBeforeEllipsis={1} nbItemsAfterEllipsis={4}>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Home
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Products
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Hosting
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Servers
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Dedicated
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Rise
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          RISE-2
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

### Custom Collapse Threshold

```tsx
{
  globals: {
    imports: `import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb collapseThreshold={7}>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Home
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Products
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Hosting
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Servers
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Dedicated
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Rise
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          RISE-2
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb>
      <BreadcrumbItem>
        <BreadcrumbLink aria-label="Home" href="#">
          <Icon name={ICON_NAME.home} />
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Parent
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Current
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

### Demo

```tsx
{
  render: arg => <Breadcrumb collapseThreshold={arg.collapseThreshold} nbItemsAfterEllipsis={arg.nbItemsAfterEllipsis} nbItemsBeforeEllipsis={arg.nbItemsBeforeEllipsis}>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Home
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Page 1
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Page 2
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Page 3
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Current page
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>,
  argTypes: orderControls({
    collapseThreshold: {
      table: {
        category: CONTROL_CATEGORY.design
      }
    },
    nbItemsAfterEllipsis: {
      table: {
        category: CONTROL_CATEGORY.design
      }
    },
    nbItemsBeforeEllipsis: {
      table: {
        category: CONTROL_CATEGORY.design
      }
    }
  })
}
```

### No Collapse

```tsx
{
  globals: {
    imports: `import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb noCollapse>
      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Home
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Products
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Hosting
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Servers
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Dedicated
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Rise
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          RISE-2
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Breadcrumb>
      <BreadcrumbItem>
        <BreadcrumbLink aria-label="Home" href="#">
          <Icon name={ICON_NAME.home} />
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Parent
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Current
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <Breadcrumb>
      <BreadcrumbItem>
        <BreadcrumbLink aria-label="Homepage" href="#">
          Home
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Products
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Hosting
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Servers
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Dedicated
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          Rise
        </BreadcrumbLink>
      </BreadcrumbItem>

      <BreadcrumbItem>
        <BreadcrumbLink href="#">
          RISE-2
        </BreadcrumbLink>
      </BreadcrumbItem>
    </Breadcrumb>
}
```

## React Components

# Button

_A **Button** component aims to initiate an action. Its text indicates the related intent, its aspect describes the importance and influence level._

## Overview

---

**Buttons** are triggerable elements that are used to set actions. They communicate calls to action to the user and allow them to interact with pages.

**Button** labels express explicitly what action will occur when the user interacts with it. Its aspect describes the importance and influence level.

This component exists in many ways : variants, sizes and colors.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Button</td></tr><tr><th scope="row">Also known as</th><td>Call To Action, CTA</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=3-23353" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/button" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-button--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Buttons** are mainly used for actions, like adding, removing, validating, etc. They are used to allow users to interact with the page or its content.

**Buttons** work with other elements on a screen to surface the most important actions the user wants to take in that context. They must respect the proximity law in order to guide the user on the action to be performed and the expected result. A **Button** label indicates what happens when the user taps the **Button**, even if it's just to acknowledge something.

**Buttons** don't usually redirect to an external page. For that, see .

### When to use a link or a button?

Links and buttons serve different purposes, and using them correctly improves both accessibility and user experience:

-   Use a link when the action navigates the user to another page or resource, either within your application or to an external site
-   Use a button when the action performs a function or triggers a behavior on the current page, like submitting a form, opening a modal, or toggling a menu

A button should not mimic a link. It can lead to confusion for users and assistive technologies, as button are not typically expected to handle navigation.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a primary Button to highlight the main action on a page or in a form |
| - Pair an outline Button for complementary actions (e.g., "Cancel", "Learn more") |
| - Maintain a clear visual hierarchy between Button types (primary, outline, ghost) |
| - Use icons when they add clarity to the action (e.g., arrow for "Next", trash for "Delete") |
| - Choose the appropriate size based on context |

| ❌ Don't |
| --- |
| - Don't use a Button as a link, use the Link component instead |
| - Don't show more than one primary button per view |
| - Don't place two identical Buttons side by side (same label and style) |
| - Don't use standalone decorative icons in Buttons unless the action is universally understood |
| - Don't use a ghost Button as the main call to action |
| - Don't overload a Button with too much text or visual clutter |

### Best Practices in Context

1.  **Button**
2.  **Icon** - optional (left or right)
3.  **Label** - optional

## Behavior

---

A **Button** can be hovered, focused, clicked and disabled.

When in disabled state, it is impossible to focus or click on it. A "disabled" cursor is shown when hovering the disabled component.

The **Button** supports a loading state.

An icon can be displayed on the left or right of the **Button** label. Icon-only **Button** also exists but it must meet accessibility requirements. See Accessibility section below.

## Variation

---

### Color

-   **Primary** _(default)_: used for main usage of **Buttons**, and linked to the brand graphical charter.
-   **Critical**: alerts users to high-priority actions or warnings that require immediate attention.
-   **Neutral**: secondary actions (such as clearing field or close button).

### Variant

-   **Default**: primary actions, featuring a full background with a matching border to signify important emphasis.
-   **Outline**: secondary actions, featuring a transparent background with a visible border to signify less emphasis than primary buttons.
-   **Ghost**: tertiary or less prominent actions, blending into the background with minimal styling until hovered or focused to reduce visual dominance.

### Size

-   **Extra Small**: actions in tiny spaces, such as action button in fields or compact rows in a table.
-   **Small**: actions in compact spaces or within densely packed interfaces, providing a more subtle and space-efficient option.
-   **Medium** _(default)_: main usage of **Buttons**, when they can be displayed in an important manner.

## Navigation

---

### Focus Management

The **Button** component can receive keyboard focus and is part of the standard tab order.

If the **Button** is disabled, it does not receive focus and cannot be activated via keyboard.

### General Keyboard Shortcuts

Pressing Enter or Space while the **Button** is focused activates it, triggering its action.

Pressing Shift + Alt moves focus to the previous interactive element.

## Accessibility

---

This component complies with the [Button WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/) .

### Always provide an explicit text content

**Button** should always have a clear and descriptive text content to indicate their purpose.

```jsx
<Button>
  Clear
</Button>
```

Screen readers will announce the text content.

### Icon Button

If a **Button** contains only an icon, it must include an [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) to provide context.

```jsx
<Button aria-label="Clear">
  <Icon name="xmark" />
</Button>
```

Screen readers will announce the label.

### Using aria-labelledby for additional context

When a **Button** is associated with an existing label, content is read by screen readers when the **Button** receives focus.

Filter your search results

```jsx
<>
  <Button aria-labelledby="filter-btn">
    <Icon name="filter" />
  </Button>
  <span id="filter-btn">
    Filter your search results  </span>
</>
```

When the **Button** is focused, the screen reader will announce the linked label.

## React Components

# Button

## Overview

---

## Anatomy

---

Button

---

## Button

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes) . |
| 
color

 | `BUTTON_COLOR` | - | `BUTTON_COLOR.primary` | The color preset to use. |
| 

loading

 | `boolean` | - | `false` | Whether the component is in loading state, disabling it. |
| 

size

 | `BUTTON_SIZE` | - | `BUTTON_SIZE.md` | The size preset to use. |
| 

variant

 | `BUTTON_VARIANT` | - | `BUTTON_VARIANT.default` | The variant preset to use. |

## Enums

---

### BUTTON_COLOR

-   critical =`"critical"`
-   information =`"information"`
-   neutral =`"neutral"`
-   primary =`"primary"`
-   success =`"success"`
-   warning =`"warning"`

### BUTTON_SIZE

-   md =`"md"`
-   sm =`"sm"`
-   xs =`"xs"`

### BUTTON_VARIANT

-   default =`"default"`
-   ghost =`"ghost"`
-   outline =`"outline"`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-button-background-color-critical | var(--ods-color-critical-500) | 
 |
| --ods-button-background-color-critical-active | var(--ods-color-critical-800) | 

 |
| --ods-button-background-color-critical-ghost | inherit | 

 |
| --ods-button-background-color-critical-ghost-active | var(--ods-color-critical-200) | 

 |
| --ods-button-background-color-critical-ghost-hover | var(--ods-color-critical-100) | 

 |
| --ods-button-background-color-critical-hover | var(--ods-color-critical-700) | 

 |
| --ods-button-background-color-critical-outline | var(--ods-color-critical-000) | 

 |
| --ods-button-background-color-critical-outline-active | var(--ods-color-critical-200) | 

 |
| --ods-button-background-color-critical-outline-hover | var(--ods-color-critical-100) | 

 |
| --ods-button-background-color-information | var(--ods-color-information-100) | 

 |
| --ods-button-background-color-information-active | var(--ods-color-information-300) | 

 |
| --ods-button-background-color-information-ghost | inherit | 

 |
| --ods-button-background-color-information-ghost-active | var(--ods-color-information-200) | 

 |
| --ods-button-background-color-information-ghost-hover | var(--ods-color-information-100) | 

 |
| --ods-button-background-color-information-hover | var(--ods-color-information-200) | 

 |
| --ods-button-background-color-information-outline | var(--ods-color-information-000) | 

 |
| --ods-button-background-color-information-outline-active | var(--ods-color-information-200) | 

 |
| --ods-button-background-color-information-outline-hover | var(--ods-color-information-100) | 

 |
| --ods-button-background-color-neutral | var(--ods-color-neutral-600) | 

 |
| --ods-button-background-color-neutral-active | var(--ods-color-neutral-800) | 

 |
| --ods-button-background-color-neutral-ghost | inherit | 

 |
| --ods-button-background-color-neutral-ghost-active | var(--ods-color-neutral-200) | 

 |
| --ods-button-background-color-neutral-ghost-hover | var(--ods-color-neutral-100) | 

 |
| --ods-button-background-color-neutral-hover | var(--ods-color-neutral-700) | 

 |
| --ods-button-background-color-neutral-outline | var(--ods-color-neutral-000) | 

 |
| --ods-button-background-color-neutral-outline-active | var(--ods-color-neutral-200) | 

 |
| --ods-button-background-color-neutral-outline-hover | var(--ods-color-neutral-100) | 

 |
| --ods-button-background-color-primary | var(--ods-color-primary-500) | 

 |
| --ods-button-background-color-primary-active | var(--ods-color-primary-800) | 

 |
| --ods-button-background-color-primary-ghost | inherit | 

 |
| --ods-button-background-color-primary-ghost-active | var(--ods-color-primary-200) | 

 |
| --ods-button-background-color-primary-ghost-hover | var(--ods-color-primary-100) | 

 |
| --ods-button-background-color-primary-hover | var(--ods-color-primary-700) | 

 |
| --ods-button-background-color-primary-outline | var(--ods-color-primary-000) | 

 |
| --ods-button-background-color-primary-outline-active | var(--ods-color-primary-200) | 

 |
| --ods-button-background-color-primary-outline-hover | var(--ods-color-primary-100) | 

 |
| --ods-button-background-color-success | var(--ods-color-success-500) | 

 |
| --ods-button-background-color-success-active | var(--ods-color-success-800) | 

 |
| --ods-button-background-color-success-ghost | inherit | 

 |
| --ods-button-background-color-success-ghost-active | var(--ods-color-success-200) | 

 |
| --ods-button-background-color-success-ghost-hover | var(--ods-color-success-100) | 

 |
| --ods-button-background-color-success-hover | var(--ods-color-success-700) | 

 |
| --ods-button-background-color-success-outline | var(--ods-color-success-000) | 

 |
| --ods-button-background-color-success-outline-active | var(--ods-color-success-200) | 

 |
| --ods-button-background-color-success-outline-hover | var(--ods-color-success-100) | 

 |
| --ods-button-background-color-warning | var(--ods-color-warning-400) | 

 |
| --ods-button-background-color-warning-active | var(--ods-color-warning-600) | 

 |
| --ods-button-background-color-warning-ghost | inherit | 

 |
| --ods-button-background-color-warning-ghost-active | var(--ods-color-warning-200) | 

 |
| --ods-button-background-color-warning-ghost-hover | var(--ods-color-warning-100) | 

 |
| --ods-button-background-color-warning-hover | var(--ods-color-warning-500) | 

 |
| --ods-button-background-color-warning-outline | var(--ods-color-warning-000) | 

 |
| --ods-button-background-color-warning-outline-active | var(--ods-color-warning-200) | 

 |
| --ods-button-background-color-warning-outline-hover | var(--ods-color-warning-100) | 

 |
| --ods-button-border-color-critical | var(--ods-color-critical-500) | 

 |
| --ods-button-border-color-critical-active | var(--ods-color-critical-800) | 

 |
| --ods-button-border-color-critical-ghost | unset | 

 |
| --ods-button-border-color-critical-ghost-active | unset | 

 |
| --ods-button-border-color-critical-ghost-hover | unset | 

 |
| --ods-button-border-color-critical-hover | var(--ods-color-critical-700) | 

 |
| --ods-button-border-color-critical-outline | var(--ods-color-critical-500) | 

 |
| --ods-button-border-color-critical-outline-active | var(--ods-color-critical-800) | 

 |
| --ods-button-border-color-critical-outline-hover | var(--ods-color-critical-700) | 

 |
| --ods-button-border-color-information | var(--ods-color-information-100) | 

 |
| --ods-button-border-color-information-active | var(--ods-color-information-300) | 

 |
| --ods-button-border-color-information-ghost | unset | 

 |
| --ods-button-border-color-information-ghost-active | unset | 

 |
| --ods-button-border-color-information-ghost-hover | unset | 

 |
| --ods-button-border-color-information-hover | var(--ods-color-information-200) | 

 |
| --ods-button-border-color-information-outline | var(--ods-color-information-500) | 

 |
| --ods-button-border-color-information-outline-active | var(--ods-color-information-800) | 

 |
| --ods-button-border-color-information-outline-hover | var(--ods-color-information-700) | 

 |
| --ods-button-border-color-neutral | var(--ods-color-neutral-600) | 

 |
| --ods-button-border-color-neutral-active | var(--ods-color-neutral-800) | 

 |
| --ods-button-border-color-neutral-ghost | unset | 

 |
| --ods-button-border-color-neutral-ghost-active | unset | 

 |
| --ods-button-border-color-neutral-ghost-hover | unset | 

 |
| --ods-button-border-color-neutral-hover | var(--ods-color-neutral-700) | 

 |
| --ods-button-border-color-neutral-outline | var(--ods-color-neutral-600) | 

 |
| --ods-button-border-color-neutral-outline-active | var(--ods-color-neutral-800) | 

 |
| --ods-button-border-color-neutral-outline-hover | var(--ods-color-neutral-700) | 

 |
| --ods-button-border-color-primary | var(--ods-color-primary-500) | 

 |
| --ods-button-border-color-primary-active | var(--ods-color-primary-800) | 

 |
| --ods-button-border-color-primary-ghost | unset | 

 |
| --ods-button-border-color-primary-ghost-active | unset | 

 |
| --ods-button-border-color-primary-ghost-hover | unset | 

 |
| --ods-button-border-color-primary-hover | var(--ods-color-primary-700) | 

 |
| --ods-button-border-color-primary-outline | var(--ods-color-primary-500) | 

 |
| --ods-button-border-color-primary-outline-active | var(--ods-color-primary-800) | 

 |
| --ods-button-border-color-primary-outline-hover | var(--ods-color-primary-700) | 

 |
| --ods-button-border-color-success | var(--ods-color-success-500) | 

 |
| --ods-button-border-color-success-active | var(--ods-color-success-800) | 

 |
| --ods-button-border-color-success-ghost | unset | 

 |
| --ods-button-border-color-success-ghost-active | unset | 

 |
| --ods-button-border-color-success-ghost-hover | unset | 

 |
| --ods-button-border-color-success-hover | var(--ods-color-success-700) | 

 |
| --ods-button-border-color-success-outline | var(--ods-color-success-500) | 

 |
| --ods-button-border-color-success-outline-active | var(--ods-color-success-800) | 

 |
| --ods-button-border-color-success-outline-hover | var(--ods-color-success-700) | 

 |
| --ods-button-border-color-warning | var(--ods-color-warning-400) | 

 |
| --ods-button-border-color-warning-active | var(--ods-color-warning-600) | 

 |
| --ods-button-border-color-warning-ghost | unset | 

 |
| --ods-button-border-color-warning-ghost-active | unset | 

 |
| --ods-button-border-color-warning-ghost-hover | unset | 

 |
| --ods-button-border-color-warning-hover | var(--ods-color-warning-500) | 

 |
| --ods-button-border-color-warning-outline | var(--ods-color-warning-700) | 

 |
| --ods-button-border-color-warning-outline-active | var(--ods-color-warning-900) | 

 |
| --ods-button-border-color-warning-outline-hover | var(--ods-color-warning-800) | 

 |
| --ods-button-border-radius-md | var(--ods-theme-border-radius) | 

 |
| --ods-button-border-radius-sm | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-button-border-radius-width | var(--ods-theme-border-width) | 

 |
| --ods-button-border-radius-xs | calc(var(--ods-theme-border-radius) / 4) | 

 |
| --ods-button-column-gap-md | var(--ods-theme-column-gap) | 

 |
| --ods-button-column-gap-sm | calc(var(--ods-theme-column-gap) * 0.75) | 

 |
| --ods-button-column-gap-xs | calc(var(--ods-theme-column-gap) * 0.75) | 

 |
| --ods-button-padding-horizontal-md | var(--ods-theme-padding-horizontal) | 

 |
| --ods-button-padding-horizontal-sm | calc(var(--ods-theme-padding-horizontal) * 0.75) | 

 |
| --ods-button-padding-horizontal-xs | calc(var(--ods-theme-padding-horizontal) * 0.75) | 

 |
| --ods-button-padding-vertical-md | var(--ods-theme-padding-vertical) | 

 |
| --ods-button-padding-vertical-sm | calc(var(--ods-theme-padding-vertical) * 0.75) | 

 |
| --ods-button-padding-vertical-xs | calc(var(--ods-theme-padding-vertical) * 0.75) | 

 |
| --ods-button-text-color-critical | var(--ods-color-critical-000) | 

 |
| --ods-button-text-color-critical-active | var(--ods-color-critical-000) | 

 |
| --ods-button-text-color-critical-ghost | var(--ods-color-critical-500) | 

 |
| --ods-button-text-color-critical-ghost-active | var(--ods-color-critical-800) | 

 |
| --ods-button-text-color-critical-ghost-hover | var(--ods-color-critical-700) | 

 |
| --ods-button-text-color-critical-hover | var(--ods-color-critical-000) | 

 |
| --ods-button-text-color-critical-outline | var(--ods-color-critical-500) | 

 |
| --ods-button-text-color-critical-outline-active | var(--ods-color-critical-800) | 

 |
| --ods-button-text-color-critical-outline-hover | var(--ods-color-critical-700) | 

 |
| --ods-button-text-color-information | var(--ods-color-information-700) | 

 |
| --ods-button-text-color-information-active | var(--ods-color-information-700) | 

 |
| --ods-button-text-color-information-ghost | var(--ods-color-information-500) | 

 |
| --ods-button-text-color-information-ghost-active | var(--ods-color-information-800) | 

 |
| --ods-button-text-color-information-ghost-hover | var(--ods-color-information-700) | 

 |
| --ods-button-text-color-information-hover | var(--ods-color-information-700) | 

 |
| --ods-button-text-color-information-outline | var(--ods-color-information-500) | 

 |
| --ods-button-text-color-information-outline-active | var(--ods-color-information-800) | 

 |
| --ods-button-text-color-information-outline-hover | var(--ods-color-information-700) | 

 |
| --ods-button-text-color-neutral | var(--ods-color-neutral-000) | 

 |
| --ods-button-text-color-neutral-active | var(--ods-color-neutral-000) | 

 |
| --ods-button-text-color-neutral-ghost | var(--ods-color-neutral-600) | 

 |
| --ods-button-text-color-neutral-ghost-active | var(--ods-color-neutral-800) | 

 |
| --ods-button-text-color-neutral-ghost-hover | var(--ods-color-neutral-700) | 

 |
| --ods-button-text-color-neutral-hover | var(--ods-color-neutral-000) | 

 |
| --ods-button-text-color-neutral-outline | var(--ods-color-neutral-600) | 

 |
| --ods-button-text-color-neutral-outline-active | var(--ods-color-neutral-800) | 

 |
| --ods-button-text-color-neutral-outline-hover | var(--ods-color-neutral-700) | 

 |
| --ods-button-text-color-primary | var(--ods-color-primary-000) | 

 |
| --ods-button-text-color-primary-active | var(--ods-color-primary-000) | 

 |
| --ods-button-text-color-primary-ghost | var(--ods-color-primary-500) | 

 |
| --ods-button-text-color-primary-ghost-active | var(--ods-color-primary-800) | 

 |
| --ods-button-text-color-primary-ghost-hover | var(--ods-color-primary-700) | 

 |
| --ods-button-text-color-primary-hover | var(--ods-color-primary-000) | 

 |
| --ods-button-text-color-primary-outline | var(--ods-color-primary-500) | 

 |
| --ods-button-text-color-primary-outline-active | var(--ods-color-primary-800) | 

 |
| --ods-button-text-color-primary-outline-hover | var(--ods-color-primary-700) | 

 |
| --ods-button-text-color-success | var(--ods-color-success-000) | 

 |
| --ods-button-text-color-success-active | var(--ods-color-success-000) | 

 |
| --ods-button-text-color-success-ghost | var(--ods-color-success-500) | 

 |
| --ods-button-text-color-success-ghost-active | var(--ods-color-success-800) | 

 |
| --ods-button-text-color-success-ghost-hover | var(--ods-color-success-700) | 

 |
| --ods-button-text-color-success-hover | var(--ods-color-success-000) | 

 |
| --ods-button-text-color-success-outline | var(--ods-color-success-500) | 

 |
| --ods-button-text-color-success-outline-active | var(--ods-color-success-800) | 

 |
| --ods-button-text-color-success-outline-hover | var(--ods-color-success-700) | 

 |
| --ods-button-text-color-warning | var(--ods-color-warning-900) | 

 |
| --ods-button-text-color-warning-active | var(--ods-color-warning-900) | 

 |
| --ods-button-text-color-warning-ghost | var(--ods-color-warning-700) | 

 |
| --ods-button-text-color-warning-ghost-active | var(--ods-color-warning-900) | 

 |
| --ods-button-text-color-warning-ghost-hover | var(--ods-color-warning-800) | 

 |
| --ods-button-text-color-warning-hover | var(--ods-color-warning-900) | 

 |
| --ods-button-text-color-warning-outline | var(--ods-color-warning-700) | 

 |
| --ods-button-text-color-warning-outline-active | var(--ods-color-warning-900) | 

 |
| --ods-button-text-color-warning-outline-hover | var(--ods-color-warning-800) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Button>
      My button    </Button>
}
```

### Color

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { BUTTON_COLOR, Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button color={BUTTON_COLOR.critical}>Critical</Button>
      <Button color={BUTTON_COLOR.information}>Information</Button>
      <Button color={BUTTON_COLOR.neutral}>Neutral</Button>
      <Button color={BUTTON_COLOR.primary}>Primary</Button>
      <Button color={BUTTON_COLOR.success}>Success</Button>
      <Button color={BUTTON_COLOR.warning}>Warning</Button>
    </>
}
```

### Loading

```jsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Button loading={true}>
      Loading button    </Button>
}
```

### Size

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { BUTTON_SIZE, Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button size={BUTTON_SIZE.md}>MD button</Button>
      <Button size={BUTTON_SIZE.sm}>SM button</Button>
      <Button size={BUTTON_SIZE.xs}>XS button</Button>
    </>
}
```

### Variant

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { BUTTON_VARIANT, Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button variant={BUTTON_VARIANT.default}>Default button</Button>
      <Button variant={BUTTON_VARIANT.outline}>Outline button</Button>
      <Button variant={BUTTON_VARIANT.ghost}>Ghost button</Button>
    </>
}
```

## Recipes

---

Chat

Assistant2:58 PM

Welcome to the Chat recipe. Feel free to test the UI behavior by typing anything on your mind below.

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

Status Message

Activate your project and get €200 in free cloud credit

You are currently in discovery mode. Activate your project to unlock your cloud resources and start building in minutes.

Status Modal

## React Components

# Button Group

 

## Overview

---

The **Button Group** component arranges multiple related button elements into a single, cohesive container. It enables users to make selections from a set of related options, either allowing one or multiple active states depending on configuration.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>ButtonGroup</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/lllHUGUkhU6rZKAracs1Ig/ODS---UI-Kit" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/button-group" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

Use a **Button Group** when multiple buttons represent a set of related options or actions.

It is commonly used for:

-   Toggle sets for text formatting or styling.
-   Filtering or grouping options within a toolbar.
-   Segmented controls in compact layouts.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Button Group for logically related actions |
| - Keep labels short |
| - Use single selection for mutually exclusive choices |
| - Use multiple selection when combined states are allowed |

| ❌ Don't |
| --- |
| - Group unrelated actions together |
| - Mix icon-only and text-only buttons inconsistently |
| - Overcrowd toolbars with too many grouped options |

### Best Practices in Context

1.  **ButtonGroup**
2.  **Inactive buttons**
3.  **Active buttons**

## Behavior

---

**Button Group** buttons can be focused and triggered. **Button Group** or its buttons can be disabled.

### Single Selection

Only one button in the group can be selected at a time.

Selecting a new button automatically deselects the previously selected one.

### Multiple Selection

Several buttons can be selected simultaneously.

Each button toggles independently between selected and unselected states.

### Unselected State

It may be allowed that no button is selected at all, when the user can clear all selections (e.g., "No filter applied").

## Navigation

---

### Focus Management

When the **Button Group** receives focus, it is set on the first button.

Disabled buttons are skipped in the focus order and cannot be activated.

Focus remains within the group when navigating between items using arrow keys.

### General Keyboard Shortcuts

Pressing Tab moves focus to the first button in the group.

Pressing Shift + Tab moves focus to the previous focusable element outside the **Button Group**.

Pressing Arrow Right moves focus to the next button in the group.

Pressing Arrow Left moves focus to the previous item in the group.

Pressing Home (or fn + Arrow Left) moves focus to the first button.

Pressing End (or fn + Arrow Right) moves focus to the last button.

Pressing Space or Enter activates or deactivates the focused button, updating the selection immediately.

## Accessibility

---

The **Button Group** component handles by itself the accessibility requirements regarding the control grouping.

Though you need to ensure that each of your items follows the .

## React Components

# Button Group

## Overview

---

## Anatomy

---

ButtonGroup

ButtonGroupItem

---

## ButtonGroup

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
defaultValue

 | `string[]` | - | `undefined` | The initial value of the selected items. Use when you don't need to control the value of the component. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

multiple

 | `boolean` | - | `undefined` | Whether multiple items can be selected at the same time. |
| 

onValueChange

 | `(detail: ButtonGroupValueChangeDetail) => void` | - | `undefined` | Callback fired when the selection changes. |
| 

size

 | `BUTTON_SIZE` | - | `undefined` | The size preset to use. |
| 

value

 | `string[]` | - | `undefined` | The controlled value of the selected items. |

## ButtonGroupItem

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

value

 | `string` |  | `undefined` | The value of the item. |

## Enums

---

### BUTTON_GROUP_SIZE

-   md =`"md"`
-   sm =`"sm"`
-   xs =`"xs"`

## Interfaces

---

### ButtonGroupValueChangeDetail

-   `value: string[]`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-button-group-item-background-color-checked-disabled | var(--ods-color-neutral-500) | 
 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ButtonGroup>
      <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
      <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
      <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
      <ButtonGroupItem value="custom">
        <Icon name={ICON_NAME.calendar} /> Custom      </ButtonGroupItem>
    </ButtonGroup>
}
```

### Disabled

 

```jsx
{
  globals: {
    imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ButtonGroup disabled>
      <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
      <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
      <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
      <ButtonGroupItem value="custom">
        <Icon name={ICON_NAME.calendar} /> Custom      </ButtonGroupItem>
    </ButtonGroup>
}
```

### Disabled Item

```jsx
{
  globals: {
    imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ButtonGroup>
      <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
      <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
      <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
      <ButtonGroupItem disabled value="custom">
        <Icon name={ICON_NAME.calendar} /> Custom      </ButtonGroupItem>
    </ButtonGroup>
}
```

### Controlled

```jsx
const [values, setValues] = useState(['hourly']);
  return <ButtonGroup onValueChange={({
    value  }) => setValues(value)} value={values}>
      <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
      <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
      <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
      <ButtonGroupItem value="custom">
        <Icon name={ICON_NAME.calendar} /> Custom      </ButtonGroupItem>
    </ButtonGroup>;
}
```

### Multiple

```jsx
{
  globals: {
    imports: `import { ButtonGroup, ButtonGroupItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ButtonGroup multiple>
      <ButtonGroupItem value="option-1">Option 1</ButtonGroupItem>
      <ButtonGroupItem value="option-2">Option 2</ButtonGroupItem>
      <ButtonGroupItem value="option-3">Option 3</ButtonGroupItem>
      <ButtonGroupItem value="option-4">Option 4</ButtonGroupItem>
    </ButtonGroup>
}
```

### Size

```jsx
{
  globals: {
    imports: `import { BUTTON_GROUP_SIZE, ButtonGroup, ButtonGroupItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>MD</p>
      <ButtonGroup size={BUTTON_GROUP_SIZE.md}>
        <ButtonGroupItem value="option-1">Option 1</ButtonGroupItem>
        <ButtonGroupItem value="option-2">Option 2</ButtonGroupItem>
        <ButtonGroupItem value="option-3">Option 3</ButtonGroupItem>
        <ButtonGroupItem value="option-4">Option 4</ButtonGroupItem>
      </ButtonGroup>
      <p>SM</p>
      <ButtonGroup size={BUTTON_GROUP_SIZE.sm}>
        <ButtonGroupItem value="option-1">Option 1</ButtonGroupItem>
        <ButtonGroupItem value="option-2">Option 2</ButtonGroupItem>
        <ButtonGroupItem value="option-3">Option 3</ButtonGroupItem>
        <ButtonGroupItem value="option-4">Option 4</ButtonGroupItem>
      </ButtonGroup>
      <p>XS</p>
      <ButtonGroup size={BUTTON_GROUP_SIZE.xs}>
        <ButtonGroupItem value="option-1">Option 1</ButtonGroupItem>
        <ButtonGroupItem value="option-2">Option 2</ButtonGroupItem>
        <ButtonGroupItem value="option-3">Option 3</ButtonGroupItem>
        <ButtonGroupItem value="option-4">Option 4</ButtonGroupItem>
      </ButtonGroup>
    </>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Button Group

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultValue` | `` | No |  | The initial value of the selected items. Use when you don't need to control the value of the component. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `multiple` | `` | No |  | Whether multiple items can be selected at the same time. |
| `onValueChange` | `` | No |  | Callback fired when the selection changes. |
| `size` | `` | No |  | The size preset to use. |
| `value` | `` | No |  | The controlled value of the selected items. |


## Subcomponents


### ButtonGroupItem



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `disabled` | `` | No |  | Whether the component is disabled. |
| `value` | `` | Yes |  | The value of the item. |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <ButtonGroup>
      <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
      <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
      <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
      <ButtonGroupItem value="custom">
        <Icon name={ICON_NAME.calendar} /> Custom
      </ButtonGroupItem>
    </ButtonGroup>
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const [values, setValues] = useState(['hourly']);
    return <ButtonGroup onValueChange={({
      value
    }) => setValues(value)} value={values}>
        <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
        <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
        <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
        <ButtonGroupItem value="custom">
          <Icon name={ICON_NAME.calendar} /> Custom
        </ButtonGroupItem>
      </ButtonGroup>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ButtonGroup>
      <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
      <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
      <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
      <ButtonGroupItem value="custom">
        <Icon name={ICON_NAME.calendar} /> Custom
      </ButtonGroupItem>
    </ButtonGroup>
}
```

### Demo

```tsx
{
  render: arg => <ButtonGroup {...arg}>
      <ButtonGroupItem value="1">Option 1</ButtonGroupItem>
      <ButtonGroupItem value="2">Option 2</ButtonGroupItem>
      <ButtonGroupItem value="3">Option 3</ButtonGroupItem>
    </ButtonGroup>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    multiple: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    size: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'BUTTON_GROUP_SIZE'
        }
      },
      control: {
        type: 'select'
      },
      options: BUTTON_GROUP_SIZES
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ButtonGroup disabled>
      <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
      <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
      <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
      <ButtonGroupItem value="custom">
        <Icon name={ICON_NAME.calendar} /> Custom
      </ButtonGroupItem>
    </ButtonGroup>
}
```

### Disabled Item

```tsx
{
  globals: {
    imports: `import { ICON_NAME, ButtonGroup, ButtonGroupItem, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ButtonGroup>
      <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
      <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
      <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
      <ButtonGroupItem disabled value="custom">
        <Icon name={ICON_NAME.calendar} /> Custom
      </ButtonGroupItem>
    </ButtonGroup>
}
```

### Multiple

```tsx
{
  globals: {
    imports: `import { ButtonGroup, ButtonGroupItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ButtonGroup multiple>
      <ButtonGroupItem value="option-1">Option 1</ButtonGroupItem>
      <ButtonGroupItem value="option-2">Option 2</ButtonGroupItem>
      <ButtonGroupItem value="option-3">Option 3</ButtonGroupItem>
      <ButtonGroupItem value="option-4">Option 4</ButtonGroupItem>
    </ButtonGroup>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <ButtonGroup>
      <ButtonGroupItem value="hourly">Hourly</ButtonGroupItem>
      <ButtonGroupItem value="daily">Daily</ButtonGroupItem>
      <ButtonGroupItem value="monthly">Monthly</ButtonGroupItem>
      <ButtonGroupItem value="custom">
        <Icon name={ICON_NAME.calendar} /> Custom
      </ButtonGroupItem>
    </ButtonGroup>
}
```

### Size

```tsx
{
  globals: {
    imports: `import { BUTTON_GROUP_SIZE, ButtonGroup, ButtonGroupItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>MD</p>
      <ButtonGroup size={BUTTON_GROUP_SIZE.md}>
        <ButtonGroupItem value="option-1">Option 1</ButtonGroupItem>
        <ButtonGroupItem value="option-2">Option 2</ButtonGroupItem>
        <ButtonGroupItem value="option-3">Option 3</ButtonGroupItem>
        <ButtonGroupItem value="option-4">Option 4</ButtonGroupItem>
      </ButtonGroup>

      <p>SM</p>
      <ButtonGroup size={BUTTON_GROUP_SIZE.sm}>
        <ButtonGroupItem value="option-1">Option 1</ButtonGroupItem>
        <ButtonGroupItem value="option-2">Option 2</ButtonGroupItem>
        <ButtonGroupItem value="option-3">Option 3</ButtonGroupItem>
        <ButtonGroupItem value="option-4">Option 4</ButtonGroupItem>
      </ButtonGroup>

      <p>XS</p>
      <ButtonGroup size={BUTTON_GROUP_SIZE.xs}>
        <ButtonGroupItem value="option-1">Option 1</ButtonGroupItem>
        <ButtonGroupItem value="option-2">Option 2</ButtonGroupItem>
        <ButtonGroupItem value="option-3">Option 3</ButtonGroupItem>
        <ButtonGroupItem value="option-4">Option 4</ButtonGroupItem>
      </ButtonGroup>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <ButtonGroup multiple>
      <ButtonGroupItem value="option-1">Option 1</ButtonGroupItem>
      <ButtonGroupItem value="option-2">Option 2</ButtonGroupItem>
      <ButtonGroupItem value="option-3">Option 3</ButtonGroupItem>
      <ButtonGroupItem value="option-4">Option 4</ButtonGroupItem>
    </ButtonGroup>
}
```

## React Components/Button

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No | BUTTON_COLOR.primary | @type=BUTTON_COLOR The color preset to use. |
| `loading` | `` | No | false | Whether the component is in loading state, disabling it. |
| `size` | `` | No | BUTTON_SIZE.md | The size preset to use. |
| `variant` | `` | No | BUTTON_VARIANT.default | The variant preset to use. |


## Examples


### Accessibility Bad Practice Icon Only

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Button, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Button>
      <Icon name={ICON_NAME.xmark} />
    </Button>
}
```

### Accessibility Bad Practice Labelled By

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Button, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button>
        <Icon name={ICON_NAME.filter} />
      </Button>
      <span>Filter your search results</span>
    </>
}
```

### Accessibility Bad Practices Role Alert

```tsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: () => {
    const [error, setError] = useState('');
    const handleClick = () => {
      setError('A critical error occurred while saving.');
    };
    return <>
        <Button onClick={handleClick}>
          Save
        </Button>

        <span style={{
        marginLeft: '1rem',
        color: 'red'
      }}>
          {error}
        </span>
      </>;
  }
}
```

### Accessibility Bad Practices Role Status

```tsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: () => {
    const [message, setMessage] = useState('');
    const handleClick = () => {
      setMessage('Copied to clipboard.');
    };
    return <>
        <Button onClick={handleClick}>
          Copy
        </Button>

        <span style={{
        marginLeft: '1rem'
      }}>
          {message}
        </span>
      </>;
  }
}
```

### Accessibility Explicit Text Content

```tsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Button>
      Clear
    </Button>
}
```

### Accessibility Icon Only

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Button, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Button aria-label='Clear'>
        <Icon name={ICON_NAME.xmark} />
    </Button>
}
```

### Accessibility Labelled By

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Button, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button aria-labelledby="filter-btn">
        <Icon name={ICON_NAME.filter} />
      </Button>
      <span id="filter-btn">Filter your search results</span>
    </>
}
```

### Accessibility Role Alert

```tsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: () => {
    const [error, setError] = useState('');
    const handleClick = () => {
      setError('A critical error occurred while saving!');
    };
    return <>
        <Button onClick={handleClick}>
          Save
        </Button>

        <span role="alert" style={{
        marginLeft: '1rem',
        color: 'red'
      }}>
          {error}
        </span>
      </>;
  }
}
```

### Accessibility Role Status

```tsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: () => {
    const [message, setMessage] = useState('');
    const handleClick = () => {
      setMessage('Copied to clipboard.');
    };
    return <>
        <Button onClick={handleClick}>
          Copy
        </Button>

        <span aria-live="polite" role="status" style={{
        marginLeft: '1rem'
      }}>
          {message}
        </span>
      </>;
  }
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Button>
      Button
    </Button>
}
```

### Color

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { BUTTON_COLOR, Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button color={BUTTON_COLOR.critical}>Critical</Button>
      <Button color={BUTTON_COLOR.information}>Information</Button>
      <Button color={BUTTON_COLOR.neutral}>Neutral</Button>
      <Button color={BUTTON_COLOR.primary}>Primary</Button>
      <Button color={BUTTON_COLOR.success}>Success</Button>
      <Button color={BUTTON_COLOR.warning}>Warning</Button>
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Button>
      My button
    </Button>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    children: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    color: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'BUTTON_COLOR'
        }
      },
      control: {
        type: 'select'
      },
      options: BUTTON_COLORS
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    loading: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    size: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'BUTTON_SIZE'
        }
      },
      control: {
        type: 'select'
      },
      options: BUTTON_SIZES
    },
    variant: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'BUTTON_VARIANT'
        }
      },
      control: {
        type: 'select'
      },
      options: BUTTON_VARIANTS
    }
  }),
  args: {
    children: 'My button'
  }
}
```

### Loading

```tsx
{
  globals: {
    imports: `import { Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Button loading={true}>
      Loading button
    </Button>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Button>
      Button
    </Button>
}
```

### Size

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { BUTTON_SIZE, Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button size={BUTTON_SIZE.md}>MD button</Button>
      <Button size={BUTTON_SIZE.sm}>SM button</Button>
      <Button size={BUTTON_SIZE.xs}>XS button</Button>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px'
  }}>
      {BUTTON_SIZES.map(size => <div key={size} style={{
      display: 'flex',
      flexDirection: 'column',
      gap: '8px'
    }}>
          {BUTTON_VARIANTS.map(variant => <div key={`${String(size)}-${String(variant)}`} style={{
        display: 'flex',
        flexFlow: 'row wrap',
        gap: '8px',
        alignItems: 'center'
      }}>
              {BUTTON_COLORS.map(color => <Button key={`${String(size)}-${String(variant)}-${String(color)}`} size={size} variant={variant} color={color}>
                  {`${String(variant)} ${String(color)}`}
                </Button>)}
            </div>)}
          <div style={{
        display: 'flex',
        flexFlow: 'row wrap',
        gap: '8px',
        alignItems: 'center'
      }}>
            {BUTTON_VARIANTS.map(variant => <Button key={`disabled-${String(size)}-${String(variant)}`} size={size} variant={variant} disabled>
                Disabled
              </Button>)}
          </div>
          <div style={{
        display: 'flex',
        flexFlow: 'row wrap',
        gap: '8px',
        alignItems: 'center'
      }}>
            {BUTTON_VARIANTS.map(variant => <Button key={`loading-${String(size)}-${String(variant)}`} size={size} variant={variant} loading>
                Loading
              </Button>)}
          </div>
        </div>)}

      <div>
        <Button>
          <Icon name={ICON_NAME.xmark} /> Close
        </Button>
      </div>
    </div>
}
```

### Variant

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { BUTTON_VARIANT, Button } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Button variant={BUTTON_VARIANT.default}>Default button</Button>
      <Button variant={BUTTON_VARIANT.outline}>Outline button</Button>
      <Button variant={BUTTON_VARIANT.ghost}>Ghost button</Button>
    </>
}
```

## React Components

# Card

_A **Card** is a versatile component designed to act as a container for various types of content, providing structure and context in a visually distinct box._

Lorem ipsum dolor sit amet, consectetur adipiscing elit.  
Interdum et malesuada fames ac ante ipsum primis in faucibus.

## Overview

---

The **Card** component will serve as a flexible container that can be used across different parts of an application to display information in a structured manner.

It is designed to be reusable, customizable, and responsive, as it can hold various elements like text, Medium, and other UI components.

It also provides a consistent layout and styling for presenting information in a visually appealing way.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Card</td></tr><tr><th scope="row">Also known as</th><td>Tile (previous name), Content Container, Product Card</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=3-28244" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/card" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-card--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

A **Card** can be used to:

-   display a wide variety of content such as a text, or other elements.
-   emphasize important content
-   visually group related content
-   guide the user toward actions
-   provide the option to navigate to more detailed content

Several elements can be used in a **Card**:

-   A title
-   A description
-   A 
-   A 
-   Extra information

All these elements are not mandatory in every **Card**.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Card to group related content or actions into a visually distinct, contained block |
| - Combine contextually related elements inside a Card (e.g., image, title, text, CTA/button) |
| - Use Card to highlight key content, such as featured items, previews, or summaries |
| - Keep the Card layout clean and scannable, using consistent spacing and visual hierarchy |
| - Limit the number of interactive elements (e.g., buttons or links) |
| - Apply Card when you need to modularize content for dashboards, lists, or repeated patterns |

| ❌ Don't |
| --- |
| - Don't overuse Card across the UI. They should enhance clarity, not create visual clutter |
| - Don't fill Card with unrelated or excessive content that breaks focus or structure |
| - Don't place too many buttons or CTAs within a single Card. Keep interactions minimal and purposeful |
| - Don't use Card to group inconsistent or unrelated components (e.g., mixing stats, forms, and media arbitrarily) |
| - Don't use Card to group inconsistent or unrelated components (e.g., mixing stats, forms, and media arbitrarily) |
| - Don't make the entire Card clickable unless it's meant to behave as a single interactive unit |
| - Don't apply inconsistent padding, borders, or elevation. All Cards should follow the same structural rules |

### Best Practices in Context

1.  **Card**
2.  **Content**

## Placement

---

**Cards** that are strongly related can be grouped in a layout group. This group can flow horizontally left to right and/or vertically top to bottom.

## Variation

---

### Color

-   **Neutral**: displaying standard content without conveying any particular status or urgency, maintaining a balanced and non-emphatic appearance.
-   **Primary**: displaying highlighted content with particular emphasis.

## Navigation

---

The **Card** component is non-interactive and does not receive keyboard focus.

## Accessibility

---

When multiple **Cards** are grouped together, it is important to ensure their structure is correctly conveyed to assistive technologies.

### Structuring groups of Cards with lists

If multiple **Cards** are displayed together (e.g., a collection of products, articles or dashboards), they should be wrapped in an unordered list of items (`<ul>` and `<li>`) to improve navigation for screen reader users.

-   Lorem ipsum dolor sit amet, consectetur adipiscing elit.  
    Interdum et malesuada fames ac ante ipsum primis in faucibus.
    
-   Lorem ipsum dolor sit amet, consectetur adipiscing elit.  
    Interdum et malesuada fames ac ante ipsum primis in faucibus.
    

```jsx
<ul
  style={{
    display: 'flex',
    gap: '16px',
    listStyleType: 'none',
    margin: 0,
    padding: 0
  }}
>
  <li>
    <Card
      style={{
        padding: '8px'
      }}
    >
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.      <br />
      Interdum et malesuada fames ac ante ipsum primis in faucibus.    </Card>
  </li>
  <li>
    <Card
      style={{
        padding: '8px'
      }}
    >
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.      <br />
      Interdum et malesuada fames ac ante ipsum primis in faucibus.    </Card>
  </li>
</ul>
```

This structure ensures that assistive technologies announce **Cards** as a list, rather than reading them as separate, unrelated announcements. Screen readers will announce the list and the number of items.

### Alternative approach with ARIA roles

When modifying the HTML structure is not possible, use [role="list"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/list_role) and [role="listitem"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/listitem_role) to mimic list semantics.

Lorem ipsum dolor sit amet, consectetur adipiscing elit.  
Interdum et malesuada fames ac ante ipsum primis in faucibus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit.  
Interdum et malesuada fames ac ante ipsum primis in faucibus.

```jsx
<div
  role="list"
  style={{
    display: 'flex',
    gap: '16px'
  }}
>
  <Card
    role="listitem"
    style={{
      padding: '8px'
    }}
  >
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.    <br />
    Interdum et malesuada fames ac ante ipsum primis in faucibus.  </Card>
  <Card
    role="listitem"
    style={{
      padding: '8px'
    }}
  >
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.    <br />
    Interdum et malesuada fames ac ante ipsum primis in faucibus.  </Card>
</div>
```

This ensures that screen readers still recognize the **Cards** as a structured list even without native `<ul>` and `<li>` elements.

## React Components

# Card

## Overview

---

## Anatomy

---

Card

---

Lorem ipsum dolor sit amet, consectetur adipiscing elit.  
Interdum et malesuada fames ac ante ipsum primis in faucibus.

## Card

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
color

 | `CARD_COLOR` | - | `CARD_COLOR.primary` | The color preset to use. |

## Enums

---

### CARD_COLOR

-   critical =`"critical"`
-   information =`"information"`
-   neutral =`"neutral"`
-   primary =`"primary"`
-   success =`"success"`
-   warning =`"warning"`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-card-border-color-critical | var(--ods-color-critical-200) | 
 |
| --ods-card-border-color-information | var(--ods-color-information-200) | 

 |
| --ods-card-border-color-neutral | var(--ods-color-neutral-200) | 

 |
| --ods-card-border-color-primary | var(--ods-color-primary-500) | 

 |
| --ods-card-border-color-success | var(--ods-color-success-200) | 

 |
| --ods-card-border-color-warning | var(--ods-color-warning-200) | 

 |
| --ods-card-border-radius | var(--ods-theme-border-radius) | 

 |
| --ods-card-border-width | var(--ods-theme-border-width) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Card } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Card>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br />Interdum et malesuada fames ac ante ipsum primis in faucibus.</p>
    </Card>
}
```

### Color

Critical

Information

Neutral

Primary

Success

Warning

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    gap: '16px'
  }}>{story()}</div>],
  globals: {
    imports: `import { CARD_COLOR, Card } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Card color={CARD_COLOR.critical}>
        <p>Critical</p>
      </Card>
      <Card color={CARD_COLOR.information}>
        <p>Information</p>
      </Card>
      <Card color={CARD_COLOR.neutral}>
        <p>Neutral</p>
      </Card>
      <Card color={CARD_COLOR.primary}>
        <p>Primary</p>
      </Card>
      <Card color={CARD_COLOR.success}>
        <p>Success</p>
      </Card>
      <Card color={CARD_COLOR.warning}>
        <p>Warning</p>
      </Card>
    </>
}
```

## Recipes

---

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Feature List Product Card

WEB HOSTING

NewBest seller

Performance

For demanding online stores and projects.

1 vCore 2,4 GHz, 2 Go RAM1 vCore 2,4 GHz, 4 Go RAM2 vCores 2,4 GHz, 8 Go RAM

From

€24.46ex. VAT/month

or €13.19 incl. VAT/monthfor a 24-month registration

Minimum 2-year registration €100 free with a 5-year registration

Installation fee:Free

-   -   Unlimited websites
        
    -   High power level
        
    -   1 domain name free for the first
        
    -   500 GB SSD storage
        
    -   1,000 email addresses
        
-   1-click CMS
    
    -   WordPress
        
    -   Joomla!
        
    -   Drupal
        
    -   Prestashop
        
-   Database
    
    -   4 x 1 GB databases
        
    -   8 GB Web Cloud Databases
        
-   Security
    
    -   Unlimited free SSL
        
    -   Anti-DDoS protection
        
    -   Anti-virus and anti-spam
        
    -   Daily backups
        
-   Performance
    
    -   99.9% observed availability
        
    -   Guaranteed resources
        
    -   Unlimited traffic
        
    -   Service continuity
        
    -   Boost option to withstand temporary traffic spikes
        
-   Support and additional services
    
    -   Git
        
    -   Standard support
        
    -   SSH access
        
    -   CDN Basic
        

Media Product Card

AI Deploy

Easily deploy machine learning models and applications into production, create your API access points with ease, and make effective predictions.

## React Components/Card

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No | CARD_COLOR.primary | @type=CARD_COLOR The color preset to use. |


## Examples


### Accessibility Alternative Grouping

```tsx
{
  globals: {
    imports: `import { Card } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <div role="list" style={{
    display: 'flex',
    gap: '16px'
  }}>
      <Card role="listitem" style={{
      padding: '8px'
    }}>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br />
        Interdum et malesuada fames ac ante ipsum primis in faucibus.
      </Card>
      <Card role="listitem" style={{
      padding: '8px'
    }}>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br />
        Interdum et malesuada fames ac ante ipsum primis in faucibus.
      </Card>
    </div>
}
```

### Accessibility Grouping

```tsx
{
  globals: {
    imports: `import { Card } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <ul style={{
    display: 'flex',
    gap: '16px',
    padding: 0,
    margin: 0,
    listStyleType: 'none'
  }}>
      <li>
        <Card style={{
        padding: '8px'
      }}>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br />
          Interdum et malesuada fames ac ante ipsum primis in faucibus.
        </Card>
      </li>
      <li>
        <Card style={{
        padding: '8px'
      }}>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br />
          Interdum et malesuada fames ac ante ipsum primis in faucibus.
        </Card>
      </li>
    </ul>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Card style={{
    padding: '8px'
  }}>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br />
      Interdum et malesuada fames ac ante ipsum primis in faucibus.
    </Card>
}
```

### Color

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    gap: '16px'
  }}>{story()}</div>],
  globals: {
    imports: `import { CARD_COLOR, Card } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Card color={CARD_COLOR.critical}>
        <p>Critical</p>
      </Card>

      <Card color={CARD_COLOR.information}>
        <p>Information</p>
      </Card>

      <Card color={CARD_COLOR.neutral}>
        <p>Neutral</p>
      </Card>

      <Card color={CARD_COLOR.primary}>
        <p>Primary</p>
      </Card>

      <Card color={CARD_COLOR.success}>
        <p>Success</p>
      </Card>

      <Card color={CARD_COLOR.warning}>
        <p>Warning</p>
      </Card>
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Card } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Card>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br />Interdum et malesuada fames ac ante ipsum primis in faucibus.</p>
    </Card>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    color: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'CARD_COLOR'
        }
      },
      control: 'select',
      options: CARD_COLORS
    },
    children: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    }
  }),
  args: {
    children: 'Hello, world!'
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Card style={{
    padding: '8px'
  }}>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br />
      Interdum et malesuada fames ac ante ipsum primis in faucibus.
    </Card>
}
```

### ThemeGenerator

```tsx
{
  name: 'ThemeGenerator',
  parameters: {
    docs: {
      disable: true
    },
    layout: 'fullscreen',
    options: {
      showPanel: false
    }
  },
  tags: ['!dev', 'hidden'],
  render: ({}) => <div style={{
    display: 'flex',
    gap: '16px'
  }}>
      {CARD_COLORS.map(color => <Card key={String(color)} color={color}>
          <p style={{
        margin: 0
      }}>{String(color)}</p>
        </Card>)}
    </div>
}
```

## React Components

# Cart

Duration

2 years€32.38

---

DNSSEC

1xSecure DNSIncluded

---

E-mail account

1xZimbra StarterIncluded

---

Duration

2 years€12.70

---

DNSSEC

1xSecure DNSIncluded

---

E-mail account

1xZimbra StarterIncluded

---

23% VAT / 2 years€13.47

---

Total€58.55

2 products

ex. VAT / year  
i.e. €XX.XX incl. VAT / year

## Overview

---

The **Cart** component provides a summary of the user’s current order. It displays the selected products, their associated options, pricing details, and the overall total before checkout.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Cart</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/9r0DZb1pFrkQnX1uAq9R6y/ODS---UI-Kit?node-id=15891-3153&amp;t=vRk1EoUTDZKyd8fc-0" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/cart" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

Use the **Cart** component to:

-   Display products and options added to the user’s order.
-   Remove a product or an individual option from the order.
-   Provide a clear breakdown of pricing, including promotions and included items.
-   Show the total amount and allow users to continue their order.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Cart sections (product header, options) to group related information clearly |
| - Use expand/collapse only for optional details that help users review their order without cluttering the interface |
| - Use promotions and included option labels to communicate value clearly |
| - Display footer lines for additional contextual information such as VAT, shipping, or billing details |
| - Use remove icon buttons consistently for products and options to allow quick corrections |

| ❌ Don't |
| --- |
| - Don’t use the Cart to display unrelated content or marketing messages |
| - Don’t include vague product or option labels since they should clearly identify the item |
| - Don’t overload the Cart with too many optional lines, which can make it hard to scan |
| - Don’t mix different types of content (e.g., recommendations) with the Cart items ? |

### Best Practices in Context

1.  **Cart**
2.  **Product header**
3.  **Product option list**
4.  **Footer**

## Placement

---

The **Cart** component is typically placed:

-   On the right side of the page on desktop layouts.
-   As a bottom sheet on mobile devices.

It should remain visible and easily accessible throughout the ordering flow.

## Behavior

---

### Desktop

-   The **Cart** is open and fully expanded by default.
-   Product can be expanded or collapsed to show or hide its options.
-   The **Cart** content is vertically scrollable when it exceeds the available space.
-   Clicking the product remove icon button removes the entire product and all associated options.
-   Clicking an option remove icon button removes only that option and updates the product price and **Cart** total.

### Mobile

-   The **Cart** is closed by default.
-   Only the handle and the **Cart** total area are visible while closed.
-   Users can swipe the handle to open the **Cart** and review order details.
-   While open, the **Cart** content is scrollable.
-   Users can swipe the handle again to close the **Cart**.
-   Remove icon buttons remain accessible within the **Cart** when open and expanded.

### Empty Cart

When no product is added:

-   Display a message indicating that the cart is empty.
-   Display a disabled "Continue my order" **Button**.
-   When the last product is removed, the **Cart** immediately transitions to the empty state.

## Navigation

---

### Focus Management

All interactive elements inside the **Cart** (expand/collapse controls, buttons) can receive keyboard focus.

The **Cart** container itself does not receive focus.

When a product header expand/collapse control is activated, the focus remains on the control and the associated product options are revealed or hidden without moving focus.

When the **Cart** is closed on mobile, only the **Cart** handle and visible interactive elements can receive focus.

When the **Cart** is open on mobile, all interactive elements inside the **Cart** become focusable.

The disabled **Button** cannot receive focus.

### General Keyboard Shortcuts

Pressing Tab or Shift + Tab moves focus sequentially through all interactive elements inside the **Cart**.

Pressing Enter or Space activates the currently focused element. This can:

-   Expand or collapse a product to show or hide its options.
-   Remove the associated product or option.
-   Trigger the **Cart** action button.

Pressing Escape collapses the **Cart** when it is expanded on mobile and returns focus to the **Cart** handle.

## Accessibility

---

### Announce product and option updates

When a product or option is added, removed, or when the total price changes, screen readers are not automatically informed.

To ensure users receive feedback, use a live region in your DOM and update its text content when the `Cart` changes.

```tsx
const liveRef = useRef<HTMLDivElement>(null);
function updateLiveRegion() {
  if (liveRef.current) {
    liveRef.current.textContent = 'Product "My product" has been added to your cart. Total is now xx.xx€.';
  }
}
return (
  <div
    ref={ liveRef }
    role="status" />
```

Screen readers will inform users of product additions, removals, and total updates through the live region.

## React Components

# Cart

## Overview

---

## Anatomy

---

Cart

CartAction

CartEmpty

CartExtraContent

CartProductGroup

CartProductGroupItem

CartTotal

---

## Cart

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

onOpenChange

 | `(detail: CartOpenChangeDetail) => void` | - | `undefined` | Callback fired when the cart open state changes (on Mobile only). |

## CartAction

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) . |

## CartEmpty

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |

## CartExtraContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |

## CartProductGroup

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
details

 | `ReactNode` | - | `undefined` | Product details node to display. |
| 

label

 | `ReactNode` |  | `undefined` | Product label node to display. |
| 

onRemove

 | `() => void` | - | `undefined` | Callback fired when the remove button is pressed. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the product group. |
| 

price

 | `ReactNode` |  | `undefined` | Formatted price node to display. |

## CartProductGroupItem

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
details

 | `ReactNode` | - | `undefined` | Product details node to display. |
| 

label

 | `ReactNode` |  | `undefined` | Product label node to display. |
| 

onRemove

 | `() => void` | - | `undefined` | Callback fired when the remove button is pressed. |
| 

price

 | `ReactNode` |  | `undefined` | Formatted price node to display. |
| 

quantity

 | `number` | - | `undefined` | Number of selected product. |

## CartTotal

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
label

 | `ReactNode` |  | `undefined` | Total label node to display. |
| 

price

 | `ReactNode` |  | `undefined` | Formatted price node to display. |
| 

priceDetails

 | `ReactNode` | - | `undefined` | Price details node to display. |
| 

totalDetails

 | `ReactNode` | - | `undefined` | Total details node to display. |

## Enums

---

### CART_I18N

-   expandButton =`"cart.expand"`
-   reduceButton =`"cart.reduce"`
-   removeProductButton =`"cart.product.remove.button"`

## Interfaces

---

### CartOpenChangeDetail

-   `open: boolean`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-cart-handle-height | 4px | 
 |
| --ods-cart-padding-horizontal | calc(var(--ods-theme-padding-horizontal) * 2) | 

 |
| --ods-cart-padding-vertical | calc(var(--ods-theme-padding-vertical) * 2) | 

 |
| --ods-cart-product-group-item-padding-horizontal | var(--ods-theme-padding-horizontal) | 

 |
| --ods-cart-product-group-item-padding-vertical | var(--ods-theme-padding-vertical) | 

 |

## Examples

---

### Default

Lorem ipsum dolor3,24 €

---

Lorem ipsum dolor13,00 €

---

Total16,24 €

```jsx
<Cart>
  <CartProductGroup label="Product" price={formatPrice(13.24)}>
    <CartProductGroupItem label="Lorem ipsum dolor" price={formatPrice(3.24)} />
    <CartProductGroupItem label="Lorem ipsum dolor" price={formatPrice(13)} />
  </CartProductGroup>
  <CartTotal label="Total" price={formatPrice(16.24)} />
  <CartAction>
    Continue my order <Icon name={ICON_NAME.arrowRight} />
  </CartAction>
</Cart>
```

### Empty

```jsx
<Cart>
  <CartEmpty>
    Your cart is empty  </CartEmpty>
  <CartAction>
    Continue my order <Icon name={ICON_NAME.arrowRight} />
  </CartAction>
</Cart>
```

### Complete

```jsx
<Cart style={{
  width: '320px'
}}>
    <CartProductGroup details="Domain" label="ods.fr" onRemove={() => {}} open price={formatPrice(32.38, 'en-GB', 'EUR')}>
      <CartProductGroupItem details="Duration" label="2 years" onRemove={() => {}} price={formatPrice(32.38, 'en-GB', 'EUR')} />
      <CartProductGroupItem details="DNSSEC" label="Secure DNS" onRemove={() => {}} price="Included" quantity={1} />
      <CartProductGroupItem details="E-mail account" label="Zimbra Starter" onRemove={() => {}} price="Included" quantity={1} />
    </CartProductGroup>
    <CartProductGroup details="Domain" label="ods-doc.fr" onRemove={() => {}} price={formatPrice(12.70, 'en-GB', 'EUR')}>
      <CartProductGroupItem details="Duration" label="2 years" onRemove={() => {}} price={formatPrice(12.70, 'en-GB', 'EUR')} />
      <CartProductGroupItem details="DNSSEC" label="Secure DNS" onRemove={() => {}} price="Included" quantity={1} />
      <CartProductGroupItem details="E-mail account" label="Zimbra Starter" onRemove={() => {}} price="Included" quantity={1} />
    </CartProductGroup>
    <CartExtraContent>
      <div style={{
      display: 'flex',
      justifyContent: 'space-between'
    }}>
        <span style={{
        fontSize: '14px',
        fontWeight: 600,
        color: 'var(--ods-theme-text-color)'
      }}>23% VAT / 2 years</span>
        <span style={{
        fontSize: '14px',
        fontWeight: 700,
        color: 'var(--ods-theme-text-color)'
      }}>{formatPrice(13.47, 'en-GB', 'EUR')}</span>
      </div>
      <Divider style={{
      marginTop: 'calc(var(--ods-theme-row-gap) * 2)'
    }} />
    </CartExtraContent>
    <CartTotal label="Total" priceDetails={<div>
          <span>ex. VAT / year</span>
          <br />
          <span>i.e. €XX.XX incl. VAT / year</span>
        </div>} totalDetails="2 products" price={formatPrice(58.55, 'en-GB', 'EUR')} />
    <CartAction>
      Continue my order <Icon name={ICON_NAME.arrowRight} />
    </CartAction>
  </Cart>
```

### Mobile

```jsx
<Cart style={{
  width: '320px'
}}>
    <CartProductGroup details="Domain" label="ods.fr" onRemove={() => {}} open price={formatPrice(32.38, 'en-GB', 'EUR')}>
      <CartProductGroupItem details="Duration" label="2 years" onRemove={() => {}} price={formatPrice(32.38, 'en-GB', 'EUR')} />
      <CartProductGroupItem details="DNSSEC" label="Secure DNS" onRemove={() => {}} price="Included" quantity={1} />
      <CartProductGroupItem details="E-mail account" label="Zimbra Starter" onRemove={() => {}} price="Included" quantity={1} />
    </CartProductGroup>
    <CartProductGroup details="Domain" label="ods-doc.fr" onRemove={() => {}} price={formatPrice(12.70, 'en-GB', 'EUR')}>
      <CartProductGroupItem details="Duration" label="2 years" onRemove={() => {}} price={formatPrice(12.70, 'en-GB', 'EUR')} />
      <CartProductGroupItem details="DNSSEC" label="Secure DNS" onRemove={() => {}} price="Included" quantity={1} />
      <CartProductGroupItem details="E-mail account" label="Zimbra Starter" onRemove={() => {}} price="Included" quantity={1} />
    </CartProductGroup>
    <CartExtraContent>
      <div style={{
      display: 'flex',
      justifyContent: 'space-between'
    }}>
        <span style={{
        fontSize: '14px',
        fontWeight: 600,
        color: 'var(--ods-theme-text-color)'
      }}>23% VAT / 2 years</span>
        <span style={{
        fontSize: '14px',
        fontWeight: 700,
        color: 'var(--ods-theme-text-color)'
      }}>{formatPrice(13.47, 'en-GB', 'EUR')}</span>
      </div>
      <Divider style={{
      marginTop: 'calc(var(--ods-theme-row-gap) * 2)'
    }} />
    </CartExtraContent>
    <CartTotal label="Total" priceDetails={<div>
          <span>ex. VAT / year</span>
          <br />
          <span>i.e. €XX.XX incl. VAT / year</span>
        </div>} totalDetails="2 products" price={formatPrice(58.55, 'en-GB', 'EUR')} />
    <CartAction>
      Continue my order <Icon name={ICON_NAME.arrowRight} />
    </CartAction>
  </Cart>
```

## Recipes

---

No recipe defined for now.

## React Components/Cart

## Subcomponents


### CartAction




### CartEmpty




### CartExtraContent




### CartProductGroup



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `details` | `` | No |  | Product details node to display. |
| `label` | `` | Yes |  | Product label node to display. |
| `onRemove` | `` | No |  | Callback fired when the remove button is pressed. |
| `price` | `` | Yes |  | Formatted price node to display. |
| `open` | `` | No |  | The controlled open state of the product group. |



### CartProductGroupItem



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `details` | `` | No |  | Product details node to display. |
| `label` | `` | Yes |  | Product label node to display. |
| `onRemove` | `` | No |  | Callback fired when the remove button is pressed. |
| `price` | `` | Yes |  | Formatted price node to display. |
| `quantity` | `` | No |  | Number of selected product. |



### CartTotal



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `label` | `` | Yes |  | Total label node to display. |
| `price` | `` | Yes |  | Formatted price node to display. |
| `priceDetails` | `` | No |  | Price details node to display. |
| `totalDetails` | `` | No |  | Total details node to display. |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    alignItems: 'start',
    gap: 'var(--ods-theme-row-gap) var(--ods-theme-column-gap)'
  }}>
      <FullExample />

      <Cart>
        <CartEmpty>
          Your cart is empty
        </CartEmpty>

        <CartAction>
          Continue my order <Icon name={ICON_NAME.arrowRight} />
        </CartAction>
      </Cart>
    </div>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Cart, CartAction, CartProductGroup, CartProductGroupItem, CartTotal, ICON_NAME, Icon, formatPrice } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <Cart>
      <CartProductGroup label="Product" price={formatPrice(13.24)}>
        <CartProductGroupItem label="Lorem ipsum dolor" price={formatPrice(3.24)} />

        <CartProductGroupItem label="Lorem ipsum dolor" price={formatPrice(13)} />
      </CartProductGroup>

      <CartTotal label="Total" price={formatPrice(16.24)} />

      <CartAction>
        Continue my order <Icon name={ICON_NAME.arrowRight} />
      </CartAction>
    </Cart>
}
```

### Demo

```tsx
{
  render: () => <FullExample />
}
```

### Empty

```tsx
{
  globals: {
    imports: `import { Cart, CartAction, CartEmpty, ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <Cart>
      <CartEmpty>
        Your cart is empty
      </CartEmpty>

      <CartAction>
        Continue my order <Icon name={ICON_NAME.arrowRight} />
      </CartAction>
    </Cart>
}
```

### Full

```tsx
{
  globals: {
    imports: `import { Cart, CartAction, CartExtraContent, CartProductGroup, CartProductGroupItem, CartTotal, Divider, ICON_NAME, Icon, formatPrice } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <Cart style={{
    width: '320px'
  }}>
      <CartProductGroup details="Domain" label="ods.fr" onRemove={() => {}} open price={formatPrice(32.38, 'en-GB', 'EUR')}>
        <CartProductGroupItem details="Duration" label="2 years" onRemove={() => {}} price={formatPrice(32.38, 'en-GB', 'EUR')} />

        <CartProductGroupItem details="DNSSEC" label="Secure DNS" onRemove={() => {}} price="Included" quantity={1} />

        <CartProductGroupItem details="E-mail account" label="Zimbra Starter" onRemove={() => {}} price="Included" quantity={1} />
      </CartProductGroup>

      <CartProductGroup details="Domain" label="ods-doc.fr" onRemove={() => {}} price={formatPrice(12.70, 'en-GB', 'EUR')}>
        <CartProductGroupItem details="Duration" label="2 years" onRemove={() => {}} price={formatPrice(12.70, 'en-GB', 'EUR')} />

        <CartProductGroupItem details="DNSSEC" label="Secure DNS" onRemove={() => {}} price="Included" quantity={1} />

        <CartProductGroupItem details="E-mail account" label="Zimbra Starter" onRemove={() => {}} price="Included" quantity={1} />
      </CartProductGroup>

      <CartExtraContent>
        <div style={{
        display: 'flex',
        justifyContent: 'space-between'
      }}>
          <span style={{
          fontSize: '14px',
          fontWeight: 600,
          color: 'var(--ods-theme-text-color)'
        }}>23% VAT / 2 years</span>
          <span style={{
          fontSize: '14px',
          fontWeight: 700,
          color: 'var(--ods-theme-text-color)'
        }}>{formatPrice(13.47, 'en-GB', 'EUR')}</span>
        </div>

        <Divider style={{
        marginTop: 'calc(var(--ods-theme-row-gap) * 2)'
      }} />
      </CartExtraContent>

      <CartTotal label="Total" priceDetails={<div>
            <span>ex. VAT / year</span>
            <br />
            <span>i.e. €XX.XX incl. VAT / year</span>
          </div>} totalDetails="2 products" price={formatPrice(58.55, 'en-GB', 'EUR')} />

      <CartAction>
        Continue my order <Icon name={ICON_NAME.arrowRight} />
      </CartAction>
    </Cart>
}
```

### Mobile

```tsx
{
  globals: {
    imports: `import { Cart, CartAction, CartExtraContent, CartProductGroup, CartProductGroupItem, CartTotal, Divider, ICON_NAME, Icon, formatPrice } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    },
    userAgent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36'
  },
  tags: ['!dev'],
  render: ({}) => <Cart style={{
    width: '320px'
  }}>
      <CartProductGroup details="Domain" label="ods.fr" onRemove={() => {}} open price={formatPrice(32.38, 'en-GB', 'EUR')}>
        <CartProductGroupItem details="Duration" label="2 years" onRemove={() => {}} price={formatPrice(32.38, 'en-GB', 'EUR')} />

        <CartProductGroupItem details="DNSSEC" label="Secure DNS" onRemove={() => {}} price="Included" quantity={1} />

        <CartProductGroupItem details="E-mail account" label="Zimbra Starter" onRemove={() => {}} price="Included" quantity={1} />
      </CartProductGroup>

      <CartProductGroup details="Domain" label="ods-doc.fr" onRemove={() => {}} price={formatPrice(12.70, 'en-GB', 'EUR')}>
        <CartProductGroupItem details="Duration" label="2 years" onRemove={() => {}} price={formatPrice(12.70, 'en-GB', 'EUR')} />

        <CartProductGroupItem details="DNSSEC" label="Secure DNS" onRemove={() => {}} price="Included" quantity={1} />

        <CartProductGroupItem details="E-mail account" label="Zimbra Starter" onRemove={() => {}} price="Included" quantity={1} />
      </CartProductGroup>

      <CartExtraContent>
        <div style={{
        display: 'flex',
        justifyContent: 'space-between'
      }}>
          <span style={{
          fontSize: '14px',
          fontWeight: 600,
          color: 'var(--ods-theme-text-color)'
        }}>23% VAT / 2 years</span>
          <span style={{
          fontSize: '14px',
          fontWeight: 700,
          color: 'var(--ods-theme-text-color)'
        }}>{formatPrice(13.47, 'en-GB', 'EUR')}</span>
        </div>

        <Divider style={{
        marginTop: 'calc(var(--ods-theme-row-gap) * 2)'
      }} />
      </CartExtraContent>

      <CartTotal label="Total" priceDetails={<div>
            <span>ex. VAT / year</span>
            <br />
            <span>i.e. €XX.XX incl. VAT / year</span>
          </div>} totalDetails="2 products" price={formatPrice(58.55, 'en-GB', 'EUR')} />

      <CartAction>
        Continue my order <Icon name={ICON_NAME.arrowRight} />
      </CartAction>
    </Cart>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <FullExample />
}
```

### ThemeGenerator

```tsx
{
  name: 'ThemeGenerator',
  parameters: {
    docs: {
      disable: true
    },
    layout: 'fullscreen',
    options: {
      showPanel: false
    }
  },
  tags: ['!dev', 'hidden'],
  render: ({}) => <FullExample />
}
```

## React Components

# Checkbox

_**Checkbox** are used for a list of options where the user may make a choice by selecting multiple options, including all or none._

Checkbox

## Overview

---

**Checkbox** are used to make a choice that must be confirmed by submitting a form. For an instantaneous choice (without submit), the use of a switch is preferred (see Switch).

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Checkbox</td></tr><tr><th scope="row">Also known as</th><td>Checkbox Button (previous name), Check Box, Tick Box</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=3-28514" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/checkbox" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-checkbox--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

**Checkbox** can be used in forms and containers.

Also, it can serve as :

-   Selecting/deselecting item(s)
-   Lists/sub-lists
-   Filters
-   Agreement to terms and conditions

A **Checkbox** group is used for a list of options where the user may make a choice by selecting multiple options, including all or none.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use clear, concise, and descriptive labels to explain what the Checkbox controls |
| - Allow labels to wrap to multiple lines when necessary, readability is more important than keeping to one line here |
| - Group related Checkboxes using a fieldset with a legend to provide context when applicable |
| - Use Checkboxes when multiple selections are allowed within a group of options |

| ❌ Don't |
| --- |
| - Don't use a Checkbox if the user can only select one option, use a Radio Group instead |
| - Don't truncate labels with ellipsis |
| - Don't place Checkboxes too close together, maintain sufficient spacing to prevent selection errors |
| - Don't use Checkboxes for binary actions like "save" or "submit", use a Button instead |

### Best Practices in Context

1.  **Checkbox**
2.  **Label**

## Placement

---

The **Checkbox** can be autonomous, as it can be labelled in a starting/ending text.

It can be inserted in containers or next to an external item.

## Behavior

---

The default behavior is that when clicking on the **Checkbox** or its linked label, the **Checkbox** is alternatively selected or deselected depending on the previous state.

The indeterminate state is used only when the **Checkbox** contains a sub-list of selections that are partially selected.

The **Checkbox** can be in an error state, but also in a disabled state.

## Navigation

---

### Focus Management

The **Checkbox** component can receive keyboard focus and is part of the standard tab order.

If the **Checkbox** is disabled, it does not receive focus and cannot be activated via keyboard.

### General Keyboard Shortcuts

Pressing Space toggles the **Checkbox** state (checked/unchecked).

## Accessibility

---

This component complies with the [Checkbox WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox) .

Vertical spacing of at least `8px` between each checkbox is advised to provide sufficient tactile and visual separation.

## React Components

# Checkbox

## Overview

---

## Anatomy

---

Checkbox

CheckboxControl

CheckboxGroup

CheckboxLabel

---

## Checkbox

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [label attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/label#attributes) . |
| 
checked

 | `boolean | 'indeterminate'` | - | `undefined` | The controlled checked state of the checkbox. |
| 

defaultChecked

 | `boolean` | - | `undefined` | The initial checked state of the checkbox. Use when you don't need to control the checked state of the checkbox. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onCheckedChange

 | `(detail: CheckboxCheckedChangeDetail) => void` | - | `undefined` | Callback fired when the checked state changes. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. |
| 

value

 | `string` | - | `undefined` | The value of form element. Useful for form submission. |

## CheckboxControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## CheckboxGroup

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultValue

 | `string[]` | - | `undefined` | The initial value of `value` when uncontrolled. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the group is disabled. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the group is in error. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onValueChange

 | `(value: string[]) => void` | - | `undefined` | Callback fired when the value changes. |
| 

readOnly

 | `boolean` | - | `undefined` | Whether the component is readonly. |
| 

value

 | `string[]` | - | `undefined` | The controlled value of the checkbox group. |

## CheckboxLabel

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |

## Interfaces

---

### CheckboxCheckedChangeDetail

-   `checked: CheckboxCheckedState`

## Unions

---

-   `CheckboxCheckedState = boolean | "indeterminate"`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Checkbox, CheckboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Checkbox>
      <CheckboxControl />
    </Checkbox>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Checkbox, CheckboxControl, CheckboxLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Checkbox disabled>
      <CheckboxControl />
      <CheckboxLabel>
        Checkbox      </CheckboxLabel>
    </Checkbox>
}
```

### Invalid

```jsx
{
  globals: {
    imports: `import { Checkbox, CheckboxControl, CheckboxLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Checkbox invalid>
      <CheckboxControl />
      <CheckboxLabel>
        Checkbox      </CheckboxLabel>
    </Checkbox>
}
```

### States

```jsx
<>
  <Checkbox checked={false}>
    <CheckboxControl />
    <CheckboxLabel>
      Unchecked    </CheckboxLabel>
  </Checkbox>
  <Checkbox checked={true}>
    <CheckboxControl />
    <CheckboxLabel>
      Checked    </CheckboxLabel>
  </Checkbox>
  <Checkbox checked="indeterminate">
    <CheckboxControl />
    <CheckboxLabel>
      Indeterminate    </CheckboxLabel>
  </Checkbox>
</>
```

### Group

I agree to the terms and conditions.

I agree to receive marketing communications.

```jsx
{
  globals: {
    imports: `import { Checkbox, CheckboxControl, CheckboxGroup, CheckboxLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <CheckboxGroup defaultValue={['marketing']} name="acknowledgments">
      <Checkbox value="term">
        <CheckboxControl />
        <CheckboxLabel>
          I agree to the terms and conditions.        </CheckboxLabel>
      </Checkbox>
      <Checkbox value="marketing">
        <CheckboxControl />
        <CheckboxLabel>
          I agree to receive marketing communications.        </CheckboxLabel>
      </Checkbox>
    </CheckboxGroup>
}
```

### Form field

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { TEXT_PRESET, Checkbox, CheckboxControl, CheckboxLabel, FormField, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Text preset={TEXT_PRESET.label}>
        Legal considerations:      </Text>
      <FormField>
        <Checkbox>
          <CheckboxControl />
          <CheckboxLabel>
            I agree to the terms and conditions.          </CheckboxLabel>
        </Checkbox>
      </FormField>
      <FormField>
        <Checkbox>
          <CheckboxControl />
          <CheckboxLabel>
            I agree to receive marketing communications.          </CheckboxLabel>
        </Checkbox>
      </FormField>
    </>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Checkbox

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `checked` | `` | No |  | The controlled checked state of the checkbox. |
| `defaultChecked` | `` | No |  | The initial checked state of the checkbox. Use when you don't need to control the checked state of the checkbox. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onCheckedChange` | `` | No |  | Callback fired when the checked state changes. |
| `required` | `` | No |  | Whether the component is required. |
| `value` | `` | No |  | The value of form element. Useful for form submission. |


## Subcomponents


### CheckboxControl




### CheckboxGroup



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultValue` | `` | No |  | The initial value of `value` when uncontrolled. |
| `disabled` | `` | No |  | Whether the group is disabled. |
| `invalid` | `` | No |  | Whether the group is in error. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onValueChange` | `` | No |  | Callback fired when the value changes. |
| `readOnly` | `` | No |  | Whether the component is readonly. |
| `value` | `` | No |  | The controlled value of the checkbox group. |



### CheckboxLabel



## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <CheckboxGroup defaultValue={['marketing']} name="acknowledgments">
      <Checkbox value="term">
        <CheckboxControl />

        <CheckboxLabel>
          I agree to the terms and conditions.
        </CheckboxLabel>
      </Checkbox>

      <Checkbox value="marketing">
        <CheckboxControl />

        <CheckboxLabel>
          I agree to receive marketing communications.
        </CheckboxLabel>
      </Checkbox>
    </CheckboxGroup>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Checkbox, CheckboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Checkbox>
      <CheckboxControl />
    </Checkbox>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Checkbox disabled={arg.disabled} invalid={arg.invalid}>
      <CheckboxControl />

      <CheckboxLabel>
        {arg.label}
      </CheckboxLabel>
    </Checkbox>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    label: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    }
  }),
  args: {
    label: 'My checkbox'
  }
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Checkbox, CheckboxControl, CheckboxLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Checkbox disabled>
      <CheckboxControl />

      <CheckboxLabel>
        Checkbox
      </CheckboxLabel>
    </Checkbox>
}
```

### Group

```tsx
{
  globals: {
    imports: `import { Checkbox, CheckboxControl, CheckboxGroup, CheckboxLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <CheckboxGroup defaultValue={['marketing']} name="acknowledgments">
      <Checkbox value="term">
        <CheckboxControl />

        <CheckboxLabel>
          I agree to the terms and conditions.
        </CheckboxLabel>
      </Checkbox>

      <Checkbox value="marketing">
        <CheckboxControl />

        <CheckboxLabel>
          I agree to receive marketing communications.
        </CheckboxLabel>
      </Checkbox>
    </CheckboxGroup>
}
```

### In Form Field

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { TEXT_PRESET, Checkbox, CheckboxControl, CheckboxLabel, FormField, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Text preset={TEXT_PRESET.label}>
        Legal considerations:
      </Text>

      <FormField>
        <Checkbox>
          <CheckboxControl />

          <CheckboxLabel>
            I agree to the terms and conditions.
          </CheckboxLabel>
        </Checkbox>
      </FormField>

      <FormField>
        <Checkbox>
          <CheckboxControl />

          <CheckboxLabel>
            I agree to receive marketing communications.
          </CheckboxLabel>
        </Checkbox>
      </FormField>
    </>
}
```

### Invalid

```tsx
{
  globals: {
    imports: `import { Checkbox, CheckboxControl, CheckboxLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Checkbox invalid>
      <CheckboxControl />

      <CheckboxLabel>
        Checkbox
      </CheckboxLabel>
    </Checkbox>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Checkbox>
      <CheckboxControl />

      <CheckboxLabel>
        Checkbox
      </CheckboxLabel>
    </Checkbox>
}
```

### States

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Checkbox, CheckboxControl, CheckboxLabel } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Checkbox checked={false}>
        <CheckboxControl />

        <CheckboxLabel>
          Unchecked
        </CheckboxLabel>
      </Checkbox>

      <Checkbox checked={true}>
        <CheckboxControl />

        <CheckboxLabel>
          Checked
        </CheckboxLabel>
      </Checkbox>

      <Checkbox checked="indeterminate">
        <CheckboxControl />

        <CheckboxLabel>
          Indeterminate
        </CheckboxLabel>
      </Checkbox>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '12px'
  }}>
      <div style={{
      display: 'flex',
      flexFlow: 'row',
      gap: '16px',
      alignItems: 'center'
    }}>
        <Checkbox>
          <CheckboxControl />
          <CheckboxLabel>Unchecked</CheckboxLabel>
        </Checkbox>
        <Checkbox disabled>
          <CheckboxControl />
          <CheckboxLabel>Unchecked disabled</CheckboxLabel>
        </Checkbox>
      </div>

      <div style={{
      display: 'flex',
      flexFlow: 'row',
      gap: '16px',
      alignItems: 'center'
    }}>
        <Checkbox checked>
          <CheckboxControl />
          <CheckboxLabel>Checked</CheckboxLabel>
        </Checkbox>
        <Checkbox checked disabled>
          <CheckboxControl />
          <CheckboxLabel>Checked disabled</CheckboxLabel>
        </Checkbox>
      </div>

      <div style={{
      display: 'flex',
      flexFlow: 'row',
      gap: '16px',
      alignItems: 'center'
    }}>
        <Checkbox checked="indeterminate">
          <CheckboxControl />
          <CheckboxLabel>Indeterminate</CheckboxLabel>
        </Checkbox>
        <Checkbox checked="indeterminate" disabled>
          <CheckboxControl />
          <CheckboxLabel>Indeterminate disabled</CheckboxLabel>
        </Checkbox>
      </div>
    </div>
}
```

## React Components

# Clipboard

_**Clipboard** component allows user to view and copy information to its **Clipboard**._

## Overview

---

**Clipboard** component is used to copy quickly and easily a text, a link and more to the **Clipboard**.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Clipboard</td></tr><tr><th scope="row">Also known as</th><td>Copy Component, Copy to Clipboard</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=26-7351" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/clipboard" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-clipboard--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Clipboard** is used to quickly and easily copy an amount of text to the user's **Clipboard**.

It can be used when it is considered that it will cause trouble for the user to select and copy a text.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the Clipboard component to allow users to easily copy non-editable text, such as tokens, or IDs |
| - Use Clipboard when copying text manually would be error-prone or tedious |
| - Use the mask/unmask toggle appropriately for sensitive content, like passwords or tokens |

| ❌ Don't |
| --- |
| - Don't use the Clipboard just to display static text, use a read-only Input or Text component instead |
| - Don't display text that should be editable in a Clipboard, this component is read-only by nature |
| - Don't place the Clipboard in contexts where copying is unnecessary or irrelevant |

### Best Practices in Context

1.  **Clipboard**
2.  **Input text**
3.  **Copy button**
4.  **Tooltip**
5.  **Show/Hide button** - optional

## Placement

---

By default, the **Clipboard** content is left-aligned in its container.

**Clipboard**'s Tooltip is right-aligned after the component by default, and vertically centered.

## Behavior

---

**Clipboard** can be focused and hovered. They can be disabled. When disabled, the component can't be hovered, focused nor clicked.

The **Clipboard** component is used as read-only, to allow users to copy a predefined text that cannot be edited directly.

Even if no visual indicator prompts the user to do so, the user can select the text directly in the Input.

The trigger for copying the Input field content to the **Clipboard** is the "copy" button.

When hovering or focusing, a Tooltip is displayed as a helper.

The clipboard masking toggled using show/hide action is permanent. Users have to click again to show/hide the Input field content.

A confirmation Tooltip is displayed (if user is still hovering the "copy" button) when **Clipboard** content has been successfully copied.

## Navigation

---

### Focus Management

The **Clipboard** receives focus as part of the natural tab order. Copy button becomes focusable immediately after the **Clipboard**.

If the **Clipboard** is disabled, it is skipped in the tab order and cannot be focused.

### General Keyboard Shortcuts

Pressing Ctrl + C (or Cmd + C on macOS) while the input field is focused copies the selected text.

Pressing Enter or Space when the copy button is focused triggers the copy action (button component behavior).

## Accessibility

---

To ensure proper accessibility, the **Clipboard** component must be correctly labeled.

### Always provide an explicit label

Every **Clipboard** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

API key:

```jsx
<FormField>
  <FormFieldLabel>
    API key:  </FormFieldLabel>
  <Clipboard value="loremipsum">
    <ClipboardControl />
    <ClipboardTrigger />
  </Clipboard>
</FormField>
```

Screen readers will announce the label, the field and its content.

## React Components

# Clipboard

## Overview

---

## Anatomy

---

Clipboard

ClipboardControl

ClipboardTrigger

---

## Clipboard

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override (see Input i18n keys). |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

onCopy

 | `() => void` | - | `undefined` | Callback fired when the input value is copied. |
| 

positionerStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |
| 

value

 | `string` | - | `undefined` | The input value. |

## ClipboardControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [input attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#attributes) . |
| 
loading

 | `boolean` | - | `undefined` | Whether the component is in loading state. |
| 

maskOption

 | `object` | - | `undefined` | Whether the masked display is active and its initial state. |
| 

enable

 | `boolean` |  | `-` | - |
| 

initialState

 | `literal` | - | `-` | - |

## ClipboardTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
labelCopy

 | `string` | - | `'Copy to clipboard'` | The initial tooltip label on copy button. |
| 

labelCopySuccess

 | `string` | - | `'Copied!'` | The tooltip label on copy button after a successful copy. |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Clipboard">
      <ClipboardControl />
      <ClipboardTrigger />
    </Clipboard>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Disabled" disabled>
      <ClipboardControl />
      <ClipboardTrigger />
    </Clipboard>
}
```

### Loading

```jsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Loading">
      <ClipboardControl loading />
      <ClipboardTrigger />
    </Clipboard>
}
```

### Masked

```jsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Masked">
      <ClipboardControl maskOption={{
      enable: true
    }} />
      <ClipboardTrigger />
    </Clipboard>
}
```

### Custom labels

```jsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Custom labels">
      <ClipboardControl />
      <ClipboardTrigger labelCopy="Click to copy" labelCopySuccess="Successfully copied" />
    </Clipboard>
}
```

## Recipes

---

Chat

Assistant2:58 PM

Welcome to the Chat recipe. Feel free to test the UI behavior by typing anything on your mind below.

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

## React Components/Clipboard

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `disabled` | `` | No |  | Whether the component is disabled. |
| `i18n` | `` | No |  | Internal translations override (see Input i18n keys). |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `onCopy` | `` | No |  | Callback fired when the input value is copied. |
| `positionerStyle` | `` | No |  | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |
| `value` | `` | No |  | The input value. |


## Subcomponents


### ClipboardControl



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `loading` | `` | No |  | Whether the component is in loading state. |
| `maskOption` | `` | No |  | Whether the masked display is active and its initial state. |



### ClipboardTrigger



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `labelCopy` | `` | No | 'Copy to clipboard' | The initial tooltip label on copy button. |
| `labelCopySuccess` | `` | No | 'Copied!' | The tooltip label on copy button after a successful copy. |


## Examples


### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        API key:
      </FormFieldLabel>

      <Clipboard value="loremipsum">
        <ClipboardControl />

        <ClipboardTrigger />
      </Clipboard>
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Clipboard">
      <ClipboardControl />

      <ClipboardTrigger />
    </Clipboard>
}
```

### Custom Labels

```tsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Custom labels">
      <ClipboardControl />

      <ClipboardTrigger labelCopy="Click to copy" labelCopySuccess="Successfully copied" />
    </Clipboard>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Clipboard">
      <ClipboardControl />

      <ClipboardTrigger />
    </Clipboard>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Clipboard disabled={arg.disabled} value={arg.value}>
      <ClipboardControl loading={arg.loading} maskOption={{
      enable: !!arg.masked
    }} />

      <ClipboardTrigger labelCopy={arg.labelCopy} labelCopySuccess={arg.labelCopySuccess} />
    </Clipboard>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    labelCopy: {
      table: {
        category: CONTROL_CATEGORY.general,
        defaultValue: {
          summary: 'Copy'
        }
      },
      control: 'text'
    },
    labelCopySuccess: {
      table: {
        category: CONTROL_CATEGORY.general,
        defaultValue: {
          summary: 'Copied'
        }
      },
      control: 'text'
    },
    loading: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    masked: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    value: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    }
  }),
  args: {
    value: 'Clipboard'
  }
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Disabled" disabled>
      <ClipboardControl />

      <ClipboardTrigger />
    </Clipboard>
}
```

### Loading

```tsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Loading">
      <ClipboardControl loading />

      <ClipboardTrigger />
    </Clipboard>
}
```

### Masked

```tsx
{
  globals: {
    imports: `import { Clipboard, ClipboardControl, ClipboardTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Masked">
      <ClipboardControl maskOption={{
      enable: true
    }} />

      <ClipboardTrigger />
    </Clipboard>
}
```

### Overview

```tsx
{
  parameters: {
    layout: 'centered'
  },
  tags: ['!dev'],
  render: ({}) => <Clipboard value="Clipboard">
      <ClipboardControl />

      <ClipboardTrigger />
    </Clipboard>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '12px',
    alignItems: 'flex-start'
  }}>
      <Clipboard value="Clipboard">
        <ClipboardControl />
        <ClipboardTrigger />
      </Clipboard>

      <Clipboard value="Masked">
        <ClipboardControl maskOption={{
        enable: true
      }} />
        <ClipboardTrigger />
      </Clipboard>

      <Clipboard value="Loading">
        <ClipboardControl loading />
        <ClipboardTrigger />
      </Clipboard>

      <Clipboard value="Disabled" disabled>
        <ClipboardControl />
        <ClipboardTrigger />
      </Clipboard>
    </div>
}
```

## React Components

# Code

_**Code** component highlights strings or small blocks of **Code** so it makes them easier to read and understand_

```
import { Text } from '@ovhcloud/ods-react';
```

## Overview

---

A **Code** component displays a string of **Code** or a small block of **Code** that can be copied to the clipboard.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Code</td></tr><tr><th scope="row">Also known as</th><td>Code snippet</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=26-7720" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/code" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-code--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Code** is mainly used for sharing examples of **Code** that can be a string or a small block of **Code**.

An optional icon **Button** may be added in order to copy its content.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the Code component to display short and reusable code snippets (e.g., command lines, config) |
| - Make sure the code is readable and scannable, with appropriate syntax highlighting |
| - Use inline code within text blocks for single tokens |
| - Wrap multiline snippets in a block format only when necessary and still concise |

| ❌ Don't |
| --- |
| - Don't use the Code component to display very long code blocks |
| - Don't use the Code component for non-code content |
| - Don't overload the UI with code examples in places where documentation would be clearer |
| - Don't use the component if the code can't be easily understood, reused, or copied |
| - Don't style code with custom formatting that breaks consistency (e.g., non-monospace fonts, shadows, etc.) |

### Best Practices in Context

1.  **Code**
2.  **Content**
3.  **Copy button** - optional
4.  **Tooltip** (when the copy button is displayed)

## Placement

---

By default, the **Code** content is left-aligned in its container.

It should be vertically aligned with other form components on a same page.

## Behavior

---

The "Copy" icon **Button** can be hovered, focused and clicked.

If the optional icon **Button** exists, when clicking on it, the **Code** content is copied to the user's clipboard.

Copy label and copy success label can be customized.

## Navigation

---

### Focus Management

The **Code** component itself is non-interactive and does not receive keyboard focus.

However, if the component includes a "Copy" action, the associated Copy button can be focused.

### General Keyboard Shortcuts

Pressing Tab moves focus to the "Copy" button if available.

Pressing Enter or Space while the "Copy" button is focused copies the code to the clipboard.

Pressing Shift + Tab moves focus to the previous interactive element.

## Accessibility

---

Screen readers will announce the embedded code and the copy button **Tooltip** content.

## React Components

# Code

## Overview

---

```
import { Text } from '@ovhcloud/ods-react';
```

## Anatomy

---

Code

---

```
import { Text } from '@ovhcloud/ods-react';
```

## Code

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
canCopy

 | `boolean` | - | `false` | Whether the copy button is displayed. |
| 

children

 | `string` |  | `undefined` | The code to display. |
| 

highlighter

 | `object` | - | `{}` | Configuration of a specific code highlighter (see beneath for more details). |
| 

language

 | `Array` |  | `-` | The programming language displayed. |
| 

theme

 | `ThemeRegistrationAny` |  | `-` | The theme to apply to the code. |
| 

labelCopy

 | `string` | - | `undefined` | The initial tooltip label on copy button. |
| 

labelCopySuccess

 | `string` | - | `undefined` | The tooltip label on copy button after a successful copy. |
| 

onCopy

 | `() => void` | - | `undefined` | Callback fired when the text is copied. |
| 

positionerStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-code-border-radius | calc(var(--ods-theme-border-radius) / 2) | 
 |
| --ods-code-padding-horizontal | calc(var(--ods-theme-padding-horizontal) * 1.5) | 

 |
| --ods-code-padding-vertical | calc(var(--ods-theme-padding-vertical) * 1.5) | 

 |
| --ods-code-primary-color | #fff | 

 |
| --ods-code-secondary-color | #000 | 

 |
| --ods-code-trigger-background-color | #fff | 

 |
| --ods-code-trigger-background-color-active | var(--ods-color-neutral-700) | 

 |
| --ods-code-trigger-background-color-hover | var(--ods-color-neutral-600) | 

 |
| --ods-code-trigger-outline-color | #fff | 

 |

## Setup custom highlighter

---

By default, `Code` provides the lightest bundle needed to display your code. But you may want to use some highlighter to provide a better visual to your end-users.

`Code` does support all [Shiki v3](https://shiki.style/) languages and themes.

You can test all languages and themes in the [Shiki playground](https://textmate-grammars-themes.netlify.app/) .

To setup an highlighter:

-   pick a [language](https://shiki.style/languages) and a [theme](https://shiki.style/themes) .
-   include both on your app side.
-   pass them to the `Code component`.

```typescript
import { Code } from '@ovhcloud/ods-react';
import lang from '@shikijs/langs/typescript';
import theme from '@shikijs/themes/nord';
const HighlightedCode = () => (
  <Code
    highlighter={{
      language: lang,
      theme: theme,
    }}>
    console.log('Hello World');
  </Code>
);
```

## Examples

---

### Default

```
console.log('Hello world');
```

```jsx
<Code>
  console.log('Hello world');
</Code>
```

### Multiline

```jsx
{
  globals: {
    imports: `import { Code } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Code>
      {`function isTargetInElement(event, element) {
  if (!element) {    return false;  }
    return element.contains(event.target) || event.composedPath().includes(element);  }`}
    </Code>
}
```

### With copy button enabled

```jsx
{
  globals: {
    imports: `import { Code } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Code canCopy>
      {`import { Text } from '@ovhcloud/ods-react';`}
    </Code>
}
```

### Custom labels

```
console.log('Hello world');
```

```jsx
<Code
  canCopy
  labelCopy="Click to copy"
  labelCopySuccess="Successfully copied"
>
  console.log('Hello world');
</Code>
```

## Recipes

---

No recipe defined for now.

## React Components/Code

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `canCopy` | `` | No | false | Whether the copy button is displayed. |
| `highlighter` | `` | No | {} | Configuration of a specific code highlighter (see beneath for more details). |
| `labelCopy` | `` | No |  | The initial tooltip label on copy button. |
| `labelCopySuccess` | `` | No |  | The tooltip label on copy button after a successful copy. |
| `onCopy` | `` | No |  | Callback fired when the text is copied. |
| `positionerStyle` | `` | No |  | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Code canCopy>
      {`import { Text } from '@ovhcloud/ods-react';`}
    </Code>
}
```

### Can Copy

```tsx
{
  globals: {
    imports: `import { Code } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Code canCopy>
      {`import { Text } from '@ovhcloud/ods-react';`}
    </Code>
}
```

### Custom Labels

```tsx
{
  globals: {
    imports: `import { Code } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Code canCopy labelCopy="Click to copy" labelCopySuccess="Successfully copied">
      console.log('Hello world');
    </Code>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Code } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Code>
      console.log('Hello world');
    </Code>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    canCopy: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    children: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    labelCopy: {
      table: {
        category: CONTROL_CATEGORY.general,
        defaultValue: {
          summary: 'Copy to clipboard'
        }
      },
      control: 'text'
    },
    labelCopySuccess: {
      table: {
        category: CONTROL_CATEGORY.general,
        defaultValue: {
          summary: 'Copied'
        }
      },
      control: 'text'
    }
  }),
  args: {
    children: `import { Text } from '@ovhcloud/ods-react';`
  }
}
```

### Highlighter

```tsx
{
  globals: {
    imports: `import { Code } from '@ovhcloud/ods-react';
import lang from '@shikijs/langs/typescript';
import theme from '@shikijs/themes/nord';`
  },
  tags: ['!dev'],
  render: ({}) => <Code highlighter={{
    language: lang,
    theme: theme
  }}>
      console.log('Hello World');
    </Code>
}
```

### Multiline

```tsx
{
  globals: {
    imports: `import { Code } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Code>
      {`function isTargetInElement(event, element) {
  if (!element) {
    return false;
  }

    return element.contains(event.target) || event.composedPath().includes(element);
  }`}
    </Code>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Code canCopy>
      {`import { Text } from '@ovhcloud/ods-react';`}
    </Code>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    maxWidth: 600
  }}>
      <Code>
        console.log('Hello world');
      </Code>

      <Code canCopy>
        {`import { Text } from '@ovhcloud/ods-react';`}
      </Code>

      <Code canCopy labelCopy="Copy" labelCopySuccess="Copied!">
        {`const sum = (a, b) => a + b;`}
      </Code>
    </div>
}
```

## React Components

# Combobox

_**Combobox** allows users to search, select, and add items from a dynamic or predefined list._

## Overview

---

**Combobox** component allows users to search for and select items from a dynamic list of suggestions or a predefined set of allowed values. It supports both single and multiple selection modes and enables users to create new entries.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Combobox</td></tr><tr><th scope="row">Also known as</th><td>Autocomplete, Dropdown Search, Autosuggest, Filterable Select</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=6046-9189" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/combobox" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-combobox--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

The **Combobox** is best suited when users need to:

-   search within a dataset and dynamically refine results
-   provide suggestions based on user input (e.g., domain names, tags, predictive search)
-   allow users to add custom values when applicable (e.g., creating tags)

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Combobox for datasets where typing helps filter results |
| - Suitable for datasets up to a few hundred entries |
| - Provide meaningful empty states |
| - Allow users to add custom entries when appropriate |

| ❌ Don't |
| --- |
| - Avoid excessive grouping of items, as too many categories can overwhelm users |
| - Avoid using combobox when the number of items is very small (except for if the user needs to be able to add their own entries), you can use Select or Radio |

## Best Practices in Context

---

1.   where the user types the search query. It displays the current input value or selected tags (multiple selection mode)
2.  **Dropdown list** displaying a scrollable list of suggested items. Items can be customized using a custom renderer.
3.   _(multiple selection mode)_ to display selected items as tags inside the Input field
4.  **Clearable**  _(optional)_ to allow users to clear the input content
5.   _(optional)_ to indicate that data is being fetched
6.  **Add entry option** _(optional)_ allows users to create new entries when no matching result is found. The label is customizable
7.  **Empty state message** is a customizable message displayed when no suggestion match the query

## Placement

---

The dropdown is positioned below the Input field when there is enough space.

The dropdown width should match the Input field width.

In multiple selection mode, the Input field height grows dynamically to accommodate selected tags.

## Behavior

---

### Triggering the dropdown

The dropdown appears when the user clicks on the input field.

### Selecting items

Selecting an item triggers a custom event, allowing integrators to process the selected value(s).

#### Single selection mode

Clicking on an item selects it, closes the dropdown, and updates the Input field value.

If the user exits the field without selecting an item, the input reverts to the placeholder or the last selected value (if any).

### Creating new entries

User can create new entries when no matching result exists. An **"Add entry"** option appears at the top of the dropdown (label is customizable).

New entries can be added by clicking on the "Add entry" option.

#### Case sensitivity rules

-   search input is case-insensitive (e.g., searching for "a" will match "A")
    
-   newly created entries are not case-sensitive
    

Users cannot create an entry that is already selected as a tag.

If a custom entry added via "Add Entry" option is removed, it does not reappear in the dropdown, as it was not part of the original list.

### Clearable Button

If the clearable option is enabled, a dedicated Button appears inside the Input field when it contains text:

-   clicking the clearable Button resets the Input field, removing any entered text or selected value(s)
-   in multiple selection mode, only the current Input text is cleared; selected tags remain

### Loading state

A Spinner can be displayed in the Input field when results are being fetched.

### Empty state

When no matching results are found, a customizable message is displayed in the dropdown.

This state can be combined with the "Add entry" option.

### Grouped items

Items can be categorized into groups in both single and multiple selection modes.

Group titles cannot be selected, clicked and are excluded from search.

## Navigation

---

### Focus management

The Input field can be focused using the Tab key. Pressing Tab again moves focus to the next element and closes the dropdown.

If the Input field is clearable, pressing Tab first moves focus to the clear button, then to the next element.

Pressing Shift + Tab moves focus to the previous interactive element without confirming any item.

### General keyboard shortcuts

Pressing Arrow Up/Arrow Down navigates through items in the dropdown.

Pressing Backspace deletes the last character in the Input field (it does not clear the entire field at once).

Pressing Enter selects the hovered item and closes the dropdown.

Pressing Escape closes the dropdown without selection.

#### Multiple selection mode

Pressing Arrow Left/Arrow Right navigates between selected tags.

Pressing Backspace while focusing a tag will remove it.

Pressing Enter while focusing a tag will remove it.

## Accessibility

---

This component complies with the [Combobox WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) .

### Always provide an explicit label

Every **Combobox** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Favorite pet:

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Favorite pet:      </FormFieldLabel>
      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]}>
        <ComboboxControl />
        <ComboboxContent />
      </Combobox>
    </FormField>
}
```

Screen readers will announce the label, the field and its content.

### Override action context

To provide more context on the interactive elements, you can provide your own custom translations to the component.

Favorite pet:

```jsx
<FormField>
  <FormFieldLabel>
    Favorite pet:  </FormFieldLabel>
  <Combobox i18n={{
  [INPUT_I18N.clearButton]: 'Clear favorite pet selection'
}} items={[{
  label: 'Dog',
  value: 'dog'
}, {
  label: 'Cat',
  value: 'cat'
}, {
  label: 'Hamster',
  value: 'hamster'
}, {
  label: 'Parrot',
  value: 'parrot'
}, {
  label: 'Spider',
  value: 'spider'
}, {
  label: 'Goldfish',
  value: 'goldfish'
}]}>
    <ComboboxControl clearable />
    <ComboboxContent />
  </Combobox>
</FormField>
```

Screen readers will announce the label, the field, its content and custom label of focused action.

## React Components

# Combobox

## Overview

---

## Anatomy

---

Combobox

ComboboxContent

ComboboxControl

---

Dog

Cat

Hamster

Parrot

Spider

Goldfish

## Combobox

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
allowCustomValue

 | `boolean` | - | `true` | Whether to allow adding a value which is not part of the items. |
| 

customFilter

 | `(label: string, query: string) => boolean` | - | `undefined` | Custom filter logic to apply to each item. |
| 

customOptionRenderer

 | `(item: ComboboxItem) => JSX.Element` | - | `undefined` | Custom render for each option item. |
| 

defaultOpen

 | `boolean` | - | `undefined` | The initial open state of the combobox. Use when you don't need to control the open state of the combobox. |
| 

defaultValue

 | `string[]` | - | `undefined` | The initial selected value(s). Use when you don't need to control the selected value(s) of the combobox. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

highlightResults

 | `boolean` | - | `false` | Whether to highlight the matching part of filtered items. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override (see Input i18n keys). |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

items

 | `ComboboxItem[]` |  | `undefined` | The list of items |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

multiple

 | `boolean` | - | `undefined` | Whether the multiple selection is allowed. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

newElementLabel

 | `string` | - | `'Add '` | Label displayed in front of a custom new value to add. |
| 

noResultLabel

 | `string` | - | `'No results found'` | Label displayed when no values match the current input value. |
| 

onInputValueChange

 | `(value: ComboboxInputValueChangeDetails) => void` | - | `undefined` | Callback fired when the input value changes. |
| 

onOpenChange

 | `(detail: ComboboxOpenChangeDetail) => void` | - | `undefined` | Callback fired when the select open state changes. |
| 

onValueChange

 | `(value: ComboboxValueChangeDetails) => void` | - | `undefined` | Callback fired when the value(s) changes. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the combobox. |
| 

overlayConfig

 | `object` | - | `undefined` | The overlay configuration. |
| 

flip

 | `boolean` | - | `-` | Whether to flip the position. |
| 

sameWidth

 | `boolean` | - | `-` | Whether to make the floating element same width as the reference element. |
| 

readOnly

 | `boolean` | - | `undefined` | Whether the component is readonly. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. /!\ Only work for single selection mode for now. |
| 

value

 | `string[]` | - | `undefined` | The controlled selected value(s). |

## ComboboxContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. |

## ComboboxControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
clearable

 | `boolean` | - | `undefined` | Whether the clear button is displayed. |
| 

loading

 | `boolean` | - | `undefined` | Whether the component is in loading state. |
| 

placeholder

 | `string` | - | `undefined` | The placeholder text to display in the input. |

## Interfaces

---

### ComboboxGroupItem<T>

-   `customRendererData?: T`
-   `label: string`
-   `options: ComboboxOptionItem[]`

### ComboboxInputValueChangeDetails

-   `inputValue: string`

### ComboboxOpenChangeDetail

-   `open: boolean`

### ComboboxOptionItem<T>

-   `customRendererData?: T`
-   `group?: string`
-   `label: string`
-   `value: string`

### ComboboxValueChangeDetails

-   `value: string[]`

## Unions

---

-   `ComboboxItem<T> = ComboboxGroupItem | ComboboxOptionItem`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-combobox-control-column-gap | calc(var(--ods-theme-column-gap) / 2) | 
 |
| --ods-combobox-control-padding-vertical | calc(var(--ods-theme-input-padding-vertical) / 2) | 

 |
| --ods-combobox-control-row-gap | calc(var(--ods-theme-row-gap) / 2) | 

 |
| --ods-combobox-group-option-padding-horizontal | calc(var(--ods-theme-input-padding-horizontal) * 3) | 

 |
| --ods-combobox-option-new-column-gap | calc(var(--ods-theme-column-gap) / 2) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl />
      <ComboboxContent />
    </Combobox>
}
```

### Clearable

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl clearable placeholder="Combobox" />
      <ComboboxContent />
    </Combobox>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox disabled items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl placeholder="Combobox" />
      <ComboboxContent />
    </Combobox>
}
```

### Readonly

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox defaultValue={['parrot']} items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]} readOnly>
      <ComboboxControl placeholder="Combobox" />
      <ComboboxContent />
    </Combobox>
}
```

### Group

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox items={[{
    label: 'Europe',
    options: [{
      label: 'France',
      value: 'fr'
    }, {
      label: 'Germany',
      value: 'de'
    }, {
      label: 'Italy',
      value: 'it'
    }]
  }, {
    label: 'Asia',
    options: [{
      label: 'China',
      value: 'cn'
    }, {
      label: 'Japan',
      value: 'jp'
    }, {
      label: 'Russia',
      value: 'ru'
    }]
  }, {
    label: 'World',
    value: 'world'
  }]}>
      <ComboboxControl placeholder="Combobox" />
      <ComboboxContent />
    </Combobox>
}
```

### Invalid

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox invalid items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }]}>
      <ComboboxControl />
      <ComboboxContent />
    </Combobox>
}
```

### Controlled

```jsx
const [value, setValue] = useState<string[]>(['dog']);
  return <>
      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]} onValueChange={details => setValue(details.value)} value={value}>
        <ComboboxControl placeholder="Select an animal" />
        <ComboboxContent />
      </Combobox>
      <div style={{
      marginTop: 8
    }}>
        <strong>Selected value:</strong> {value[0] ?? 'None'}
      </div>
    </>;
}
```

### Highlight

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox highlightResults items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl />
      <ComboboxContent />
    </Combobox>
}
```

### Custom Options

```jsx
type MyData = {
    color?: string;
    info?: string;
  };
  const items = [{
    label: 'Apple',
    value: 'apple',
    customRendererData: {
      color: 'red',
      info: 'Fruit'
    }
  }, {
    label: 'Banana',
    value: 'banana',
    customRendererData: {
      color: 'yellow',
      info: 'Fruit'
    }
  }, {
    label: 'Carrot',
    value: 'carrot',
    customRendererData: {
      color: 'orange',
      info: 'Vegetable'
    }
  }, {
    label: 'Broccoli',
    value: 'broccoli',
    customRendererData: {
      color: 'green',
      info: 'Vegetable'
    }
  }, {
    label: 'Blueberry',
    value: 'blueberry',
    customRendererData: {
      color: 'blue',
      info: 'Fruit'
    }
  }];
  function customOptionRenderer(item: ComboboxItem<MyData>) {
    return <span style={{
      color: item.customRendererData?.color,
      fontWeight: 'bold'
    }}>
        {item.label} {item.customRendererData?.info && <span style={{
        fontWeight: 'normal',
        fontSize: 12,
        color: '#888'
      }}>({item.customRendererData.info})</span>}
      </span>;
  }
  return <Combobox customOptionRenderer={customOptionRenderer} highlightResults items={items}>
      <ComboboxControl />
      <ComboboxContent />
    </Combobox>;
}
```

### Custom Filtering

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox customFilter={(label, inputValue) => {
    const reversedLabel = label.split('').reverse().join('');
    return new RegExp(`^${inputValue}`, 'i').test(reversedLabel);
  }} items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl placeholder="Search from right to left in each word" />
      <ComboboxContent />
    </Combobox>
}
```

### Empty

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox items={[]}>
      <ComboboxControl />
      <ComboboxContent />
    </Combobox>
}
```

### Multiple

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox multiple items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl />
      <ComboboxContent />
    </Combobox>
}
```

### Form field

```jsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Combobox      </FormFieldLabel>
      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <ComboboxControl />
        <ComboboxContent />
      </Combobox>
    </FormField>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Combobox

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `allowCustomValue` | `` | No | true | Whether to allow adding a value which is not part of the items. |
| `customFilter` | `` | No |  | Custom filter logic to apply to each item. |
| `customOptionRenderer` | `` | No |  | Custom render for each option item. |
| `defaultOpen` | `` | No |  | The initial open state of the combobox. Use when you don't need to control the open state of the combobox. |
| `defaultValue` | `` | No |  | The initial selected value(s). Use when you don't need to control the selected value(s) of the combobox. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `highlightResults` | `` | No | false | Whether to highlight the matching part of filtered items. |
| `i18n` | `` | No |  | Internal translations override (see Input i18n keys). |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `items` | `` | Yes |  | The list of items |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `multiple` | `` | No |  | Whether the multiple selection is allowed. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `newElementLabel` | `` | No | 'Add ' | Label displayed in front of a custom new value to add. |
| `noResultLabel` | `` | No | 'No results found' | Label displayed when no values match the current input value. |
| `onInputValueChange` | `` | No |  | Callback fired when the input value changes. |
| `onOpenChange` | `` | No |  | Callback fired when the select open state changes. |
| `onValueChange` | `` | No |  | Callback fired when the value(s) changes. |
| `open` | `` | No |  | The controlled open state of the combobox. |
| `overlayConfig` | `` | No |  | The overlay configuration. |
| `readOnly` | `` | No |  | Whether the component is readonly. |
| `required` | `` | No |  | Whether the component is required. /!\ Only work for single selection mode for now. |
| `value` | `` | No |  | The controlled selected value(s). |


## Subcomponents


### ComboboxContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. |



### ComboboxControl



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `clearable` | `` | No |  | Whether the clear button is displayed. |
| `loading` | `` | No |  | Whether the component is in loading state. |
| `placeholder` | `` | No |  | The placeholder text to display in the input. |


## Examples


### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Favorite pet:
      </FormFieldLabel>

      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]}>
        <ComboboxControl />

        <ComboboxContent />
      </Combobox>
    </FormField>
}
```

### Accessibility I 18 N

```tsx
{
  globals: {
    imports: `import { INPUT_I18N, Combobox, ComboboxContent, ComboboxControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => <FormField>
      <FormFieldLabel>
        Favorite pet:
      </FormFieldLabel>

      <Combobox i18n={{
      [INPUT_I18N.clearButton]: 'Clear favorite pet selection'
    }} items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]}>
        <ComboboxControl clearable />

        <ComboboxContent />
      </Combobox>
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  parameters: {
    layout: 'start'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    height: '230px'
  }}>
      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]} open overlayConfig={{
      flip: false
    }}>
        <ComboboxControl placeholder="Combobox" />

        <ComboboxContent createPortal={false} />
      </Combobox>
    </div>
}
```

### Clearable

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl clearable placeholder="Combobox" />

      <ComboboxContent />
    </Combobox>
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [value, setValue] = useState<string[]>(['dog']);
    return <>
        <Combobox items={[{
        label: 'Dog',
        value: 'dog'
      }, {
        label: 'Cat',
        value: 'cat'
      }, {
        label: 'Hamster',
        value: 'hamster'
      }, {
        label: 'Parrot',
        value: 'parrot'
      }, {
        label: 'Spider',
        value: 'spider'
      }, {
        label: 'Goldfish',
        value: 'goldfish'
      }]} onValueChange={details => setValue(details.value)} value={value}>
          <ComboboxControl placeholder="Select an animal" />

          <ComboboxContent />
        </Combobox>

        <div style={{
        marginTop: 8
      }}>
          <strong>Selected value:</strong> {value[0] ?? 'None'}
        </div>
      </>;
  }
}
```

### Custom Filter

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox customFilter={(label, inputValue) => {
    const reversedLabel = label.split('').reverse().join('');
    return new RegExp(`^${inputValue}`, 'i').test(reversedLabel);
  }} items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl placeholder="Search from right to left in each word" />

      <ComboboxContent />
    </Combobox>
}
```

### Custom Options

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    type MyData = {
      color?: string;
      info?: string;
    };
    const items = [{
      label: 'Apple',
      value: 'apple',
      customRendererData: {
        color: 'red',
        info: 'Fruit'
      }
    }, {
      label: 'Banana',
      value: 'banana',
      customRendererData: {
        color: 'yellow',
        info: 'Fruit'
      }
    }, {
      label: 'Carrot',
      value: 'carrot',
      customRendererData: {
        color: 'orange',
        info: 'Vegetable'
      }
    }, {
      label: 'Broccoli',
      value: 'broccoli',
      customRendererData: {
        color: 'green',
        info: 'Vegetable'
      }
    }, {
      label: 'Blueberry',
      value: 'blueberry',
      customRendererData: {
        color: 'blue',
        info: 'Fruit'
      }
    }];
    function customOptionRenderer(item: ComboboxItem<MyData>) {
      return <span style={{
        color: item.customRendererData?.color,
        fontWeight: 'bold'
      }}>
          {item.label} {item.customRendererData?.info && <span style={{
          fontWeight: 'normal',
          fontSize: 12,
          color: '#888'
        }}>({item.customRendererData.info})</span>}
        </span>;
    }
    return <Combobox customOptionRenderer={customOptionRenderer} highlightResults items={items}>
        <ComboboxControl />

        <ComboboxContent />
      </Combobox>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl />

      <ComboboxContent />
    </Combobox>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Combobox items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]} allowCustomValue={arg.allowCustomValue} disabled={arg.disabled} highlightResults={arg.highlightResults} invalid={arg.invalid} multiple={arg.multiple} newElementLabel={arg.newElementLabel} noResultLabel={arg.noResultLabel} readOnly={arg.readOnly}>
      <ComboboxControl clearable={arg.clearable} loading={arg.loading} placeholder={arg.placeholder} />

      <ComboboxContent />
    </Combobox>,
  argTypes: orderControls({
    allowCustomValue: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    clearable: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    highlightResults: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    loading: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    multiple: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    newElementLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    noResultLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    placeholder: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    }
  }),
  args: {
    placeholder: 'Start typing'
  }
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox disabled items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl placeholder="Combobox" />

      <ComboboxContent />
    </Combobox>
}
```

### Empty

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox items={[]}>
      <ComboboxControl />

      <ComboboxContent />
    </Combobox>
}
```

### Group

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox items={[{
    label: 'Europe',
    options: [{
      label: 'France',
      value: 'fr'
    }, {
      label: 'Germany',
      value: 'de'
    }, {
      label: 'Italy',
      value: 'it'
    }]
  }, {
    label: 'Asia',
    options: [{
      label: 'China',
      value: 'cn'
    }, {
      label: 'Japan',
      value: 'jp'
    }, {
      label: 'Russia',
      value: 'ru'
    }]
  }, {
    label: 'World',
    value: 'world'
  }]}>
      <ComboboxControl placeholder="Combobox" />

      <ComboboxContent />
    </Combobox>
}
```

### Highlight

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox highlightResults items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl />

      <ComboboxContent />
    </Combobox>
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Combobox
      </FormFieldLabel>

      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <ComboboxControl />

        <ComboboxContent />
      </Combobox>
    </FormField>
}
```

### Invalid

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox invalid items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }]}>
      <ComboboxControl />

      <ComboboxContent />
    </Combobox>
}
```

### Multiple

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox multiple items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl />

      <ComboboxContent />
    </Combobox>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Combobox items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <ComboboxControl placeholder="Combobox" />

      <ComboboxContent />
    </Combobox>
}
```

### Readonly

```tsx
{
  globals: {
    imports: `import { Combobox, ComboboxContent, ComboboxControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Combobox defaultValue={['parrot']} items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]} readOnly>
      <ComboboxControl placeholder="Combobox" />

      <ComboboxContent />
    </Combobox>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px'
  }}>
      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <ComboboxControl placeholder="Default" />
        <ComboboxContent createPortal={false} />
      </Combobox>

      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <ComboboxControl clearable placeholder="Clearable" />
        <ComboboxContent createPortal={false} />
      </Combobox>

      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <ComboboxControl loading placeholder="Loading" />
        <ComboboxContent createPortal={false} />
      </Combobox>

      <Combobox disabled items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <ComboboxControl placeholder="Disabled" />
        <ComboboxContent createPortal={false} />
      </Combobox>

      <Combobox readOnly items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <ComboboxControl placeholder="Read only" />
        <ComboboxContent createPortal={false} />
      </Combobox>

      <Combobox items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]} multiple>
        <ComboboxControl placeholder="Multiple" />
        <ComboboxContent createPortal={false} />
      </Combobox>
    </div>
}
```

## React Components

# Data Table

| 
First Name

 | 

Last Name

 | 

Age

 | 

Email

 | 

Role

 |
| --- | --- | --- | --- | --- |
| John | Doe | 30 | john.doe@example.com | Admin |
| Jane | Smith | 25 | jane.smith@example.com | User |
| Bob | Johnson | 35 | bob.johnson@example.com | Manager |

## Overview

---

The `Data Table` component provides a structured and flexible way to display, explore, and interact with tabular data.

It is designed as a core component, intended to support a wide range of use cases while remaining composable, scalable, andproduct-agnostic.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>DataTable</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/KKF2yPJ1fGICU7z9usG7Pl/ODS---UI-Kit?node-id=16269-23952&amp;p=f&amp;t=AvtT3fJhAIPZ0qbf-0" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/data-table" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The `Data Table` is used to present collections of structured data and enable users to:

-   Browse and scan information efficiently.
-   Sort and organize data.
-   Select one or multiple rows.
-   Perform actions on individual row or in bulk.
-   Interact with large datasets through pagination, search, and refresh.

Typical use cases include:

-   Administration panels.
-   Management interfaces.
-   Reporting and operational dashboards.
-   Configuration screens.

### Data Table vs Table

`Table`:

-   Static data display.
-   Limited or no interaction.
-   Often used for simple layouts or read-only content.

`Data Table`:

-   Interactive and stateful component.
-   Supports sorting, selection, and actions.
-   Integrated with application logic through composition.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Data Table for managing collections of structured data |
| - Use composition to inject business-specific actions and logic |
| - Combine selection with bulk actions for efficiency |

| ❌ Don't |
| --- |
| - Use a Data Table for simple, static content |
| - Overload the table with too many visible actions |
| - Trigger destructive actions without confirmation |
| - Assume all rows behave identically in bulk operations |

### Best Practices in Context

1.  **Data Table**
2.  **Header cell**
3.  **Body cell**
4.  **Column sorting**

## Placement

---

The `Data Table` is typically placed as the main content of a page or section.

It should:

-   Expand horizontally to fit its container.
-   Manage internal scrolling when necessary.
-   Not overflow or break surrounding layouts.

## Behavior

---

### Data & State Ownership

The `Data Table` does not own data.

Data fetching, transformation, and persistence are handled by the integrator.

The component is responsible for rendering the provided dataset and reflecting externally managed states such as loading, sorting, selection, and pagination.

The integrator is responsible for:

-   Providing the data to display.
-   Handling data fetching and refreshing.
-   Managing all business rules and permissions.
-   Synchronizing external state with the `Data Table` visual state.

### Row Selection

The `Data Table` supports row selection via checkboxes or radio buttons.

The `Data Table` is responsible for:

-   Rendering selection controls.
-   Handling user interaction for selecting and deselecting rows.
-   Visually reflecting the current selection state.
-   Exposing selection changes to the integrator.

The integrator is responsible for:

-   Storing and managing the selection state.
-   Defining whether selection is single or multiple.
-   Deciding how selection behaves accross pagination or refresh.

Disabled rows are not selectable.

### Column Sorting

The `Data Table` supports column-based sorting through interactions with column headers.

The `Data Table` is responsible for:

-   Rendering sortable and non-sortable column headers.
-   Managing user interactions on sortable headers.
-   Displaying visual indicators for sorting state (unsorted, ascending, descending).

The integrator is reponsible for:

-   Implemeting the sorting logic (if done server-side).
-   Synchronizing the active sorting state with the `Data Table`.

The `Data Table` can support either single-column sorting, where only one column can be sorted at a time, or multi-column sorting, where users can sort by multiple columns simultaneously.

Non-sortable columns don't react to sorting interactions.

### Pinned columns

The `Data Table` supports pinning columns to keep them visible during horizontal scrolling.

The `Data Table` is responsible for:

-   Maintaining the pinned column position during scroll.
-   Providing a visual separation between pinned and scrollable columns.

The integrator is responsible for:

-   Defining which columns are pinned.
-   Ensuring that pinned columns are used appropriately (e.g. selection or actions).

Pinned columns remain fully interactive and accessible.

### Disabled Rows

The `Data Table` supports disabled rows to represent non-interactive or restricted items.

The `Data Table` is responsible for:

-   Rendering disabled rows with a distinct visual style.
-   Preventing interaction with disabled rows.

The integrator is responsible for:

-   Defining which rows are disabled and why.
-   Optionnaly providing explanatory messaging (e.g. tooltip).

Disabled rows must not participate in selection or bulk actions.

### Sticky Header

The `Data Table` may support sticky headers to maintain access to key controls during vertical scrolling.

The `Data Table` is responsible for:

-   Keeping the header visually fixed within the table container.

The integrator is responsible for:

-   Deciding whether sticky behavior is enabled.
-   Ensuring that injected content (actions, menus, tooltips) behave correctly within the sticky area.

Sticky behavior must not interfere with interactions such as sorting, tooltips, or menus.

### Row-level Actions (Ellipsis Menu)

Rows may expose contextual actions via an ellipsis menu.

The integrator is responsible for:

-   Rendering the menu trigger.
-   Providing the menu content.
-   Defining actions behavior and permissions.
-   Managing disabled or loading states per action.

### Global Table Actions

A dedicated area associated with the `Data Table` for global actions, intended to host at least one primary action (e.g., "Create", "Add", "Import"), but may contain additional actions if required by the product context.

The integrator is responsible for:

-   Defining the actions themselves.
-   Handling their behavior, permissions, and side effects.
-   Supporting disabled and loading visual states.

### Bulk actions

Bulk actions are actions applied to a set of selected rows.

The integrator is responsible for:

-   Rendering the bulk actions area when one or more rows are selected.
-   Defining bulk actions and their behavior.
-   Handling business rules and permissions.
-   Determining the eligibility of selected rows for each action.

When a selection includes rows with mixed eligibility, the `Data Table` must support integrator-defined strategies such as disabling the action, allowing partial execution with confirmation, or dinamycally adjusting available actions.

### Header tooltips

Displaying tooltips attached to column headers.

The integrator is responsible for:

-   Rendering tooltip triggers.
-   Providing tooltip content.

Tooltips must not obstruct sorting or other header interactions.

### Search

Associating a global search input to the `Data Table`.

The integrator is responsible for:

-   Providing the search input.
-   Implementing search logic.
-   Visually indicating when a search is active.
-   Handling empty and loading states resulting from search.

### Pagination

Adding paginated data display to the `Data Table`.

The integrator is responsible for:

-   Rendering pagination controls.
-   Displaying the current page state.
-   Providing pagination data (current page, total pages, total items).
-   Handling page changes and data updates.

The behavior of selection across page changes must be explicitly defined by the integrator.

### Refresh

Exposing a refresh control allowing users to explicitly request a data reload.

The integrator is responsible for:

-   Rendering the refresh trigger.
-   Displaying a loading state when refresh is in progress.
-   Implementing the refresh logic.
-   Updating the data and loading state accordingly.

When a refresh is triggered while a loading state is already active, the refresh action must be disabled or ignored to prevent duplicate requests.

If rows were selected prior to a refresh, it is recommended to reset them.

### Tags assignment

Supporting tag assignment workflows through composition, using a modal.

Tag assignment is treated as a business-specific action and is not implemented by the `Data Table` component.

The tag assignment modal may be triggered from different entry points, such as a row-level action for example.

The integrator is responsible for:

-   Defining when an where the tag assignment action is available.
-   Rendering the modal and its trigger.
-   Handling the association between rows and tags.
-   Providing existing tags to the modal.
-   Resolving conflicts with already assigned tags.

All error handling, messaging, and resolution strategies are the responsibility of the integrator.

### Editable Content

Allowing editable content to be injected through composition to the `Data Table`.

The integrator is responsible for:

-   Rendering editable content/component (ODS or custom).
-   Managing side effects of editing (data updates, refresh, notifications).

## Navigation

---

### Focus Management

The `Data Table` is fully navigable using the keyboard.

When focus enters the `Data Table`, it is placed on the first focusable element within the table, depending on the current configuration. This may include:

-   a global table action.
-   a sortable column header.
-   a row selection checkbox.
-   a focusable action within the first row.

Focus is never trapped inside the `Data Table`. Users can move focus into and out of the component using standard keyboard navigation.

If a header is not sortable or interactive, it must not receive focus.

Disabled rows do not receive focus.

### General Keyboard Shortcuts

#### Table Navigation

Keyboard navigation within the `Data Table` follows a predictable, linear order:

-   Pressing Tab moves focus forward through focusable elements inside the table.
-   Pressing Shift + Tab moves focus backward.

#### Column Header Navigation

When a column header is focusable:

-   Pressing Enter or Space triggers the primary header action (e.g. sorting).
-   Tooltip triggers in headers are reachable via keyboard focus.

#### Row Selection via Keyboard

Row selection controls are keyboard accessible.

-   Pressing Space toggles the selection state of the focused row checkbox.
-   The "select all" checkbox (when present) can be toggled using Space.

Disabled rows cannot be selected via keyboard.

#### Row-Level Actions

Row-level actions (such as ellipsis menus) are reachable via keyboard navigation (Space or Enter depending on the focusabled element).

## Accessibility

---

To ensure the `Data Table` component is fully accessible, it is essential to follow best practices for structuring tables with the correct attributes and a global caption to provide users with context. Please follow the [W3C technique](https://www.w3.org/TR/WCAG20-TECHS/H63) for integrating them correctly.

We also recommend referring to the [WCAG guidelines](https://www.w3.org/WAI/tutorials/tables/) for building accessible `Data Tables`.

## React Components

# Data Table

## Overview

---

## Anatomy

---

DataTable

DataTableBody

DataTableEmpty

DataTableHead

---

## DataTable

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [table attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/table) . |
| 
columns

 | `DataTableColumnDef<T>[]` |  | `undefined` | The columns definitions. |
| 

data

 | `T[]` |  | `undefined` | The table data to display. |
| 

enableMultiRowSelection

 | `boolean` | - | `true` | Whether the multi row selection is enabled. |
| 

enableRowSelection

 | `boolean | ((row: DataTableRow<T>) => boolean)` | - | `undefined` | Whether the row selection is enabled. |
| 

enableSorting

 | `boolean` | - | `true` | Whether the column sorting is enabled. |
| 

getRowId

 | `(originalRow: T, index: number, parent?: DataTableRow<T>) => string` | - | `undefined` | By default, the component will look for an id attribute in a data item to identify each row. Using this function, you can define another attribute to use instead (like uuid). |
| 

loading

 | `boolean` | - | `undefined` | Whether the table data are in a loading state. This will replace each cell with a Skeleton. |
| 

manualSorting

 | `boolean` | - | `undefined` | Whether the sorting is handled outside of the table. Use this is the sorting is managed on server-side. |
| 

onColumnPinningChange

 | `(updaterOrValue: T | ((old: T) => T)) => void` | - | `undefined` | Callback fired when the pinned columns changes. |
| 

onColumnVisibilityChange

 | `(updaterOrValue: T | ((old: T) => T)) => void` | - | `undefined` | Callback fired when the column visibility changes. |
| 

onRowSelectionChange

 | `(updaterOrValue: T | ((old: T) => T)) => void` | - | `undefined` | Callback fired when the selected rows changes. |
| 

onSortingChange

 | `(updaterOrValue: T | ((old: T) => T)) => void` | - | `undefined` | Callback fired when the columns sorting changes. |
| 

state

 | `DataTableState` | - | `undefined` | The controlled table values. |
| 

stickyHeader

 | `boolean` | - | `undefined` | Whether the header should be sticky. |

## DataTableBody

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [tbody attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/tbody) . |

## DataTableEmpty

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [tbody attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/tbody) . |

## DataTableHead

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [thead attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/thead) . |

## Interfaces

---

### DataTableCell<T>

-   `column: DataTableColumn<T>`
-   `getValue: <TValue>() => TValue`
-   `id: string`
-   `row: DataTableRow<T>`

### DataTableCellContext<T>

-   `cell: DataTableCell<T>`
-   `column: DataTableColumn<T>`
-   `table: DataTableTable<T>`

### DataTableColumn<T>

-   `clearSorting: () => void`
-   `columnDef: Omit<DataTableColumnDef, 'accessorKey'>`
-   `getCanSort: () => boolean`
-   `getIsPinned: () => false | left | right`
-   `getIsSorted: () => false | asc | desc`
-   `getIsVisible: () => boolean`
-   `getPinnedIndex: () => number`
-   `id: string`

### DataTableColumnDef<T>

-   `accessorKey?: string | keyof T`
-   `cell?: string | (cellContext: DataTableCellContext<T>) => ReactNode`
-   `enableSorting?: boolean`
-   `header?: string | (headerContext: DataTableHeaderContext<T>) => ReactNode`
-   `id: string`
-   `sortingFn?: (rowA: DataTableRow<T>, rowB: DataTableRow<T>, columnId: string) => number`
-   `size?: number`

### DataTableColumnPinningState

-   `left?: string[]`
-   `right?: string[]`

### DataTableColumnVisibilityState

-   `Record<string, boolean>`

### DataTableHeader<T>

-   `column: DataTableColumn<T>`
-   `id: string`

### DataTableHeaderContext<T>

-   `column: DataTableColumn<T>`
-   `header: DataTableHeader<T>`
-   `table: DataTableTable<T>`

### DataTableRow<T>

-   `getValue: <TValue>(columnId: string) => TValue`
-   `index: number`
-   `original: T`

### DataTableRowSelectionState

-   `Record<string, boolean>`

### DataTableSortingState

-   `{ desc: boolean, id: string }[]`

### DataTableState

-   `columnPinning?: DataTableColumnPinningState`
-   `columnVisibility?: DataTableColumnVisibilityState`
-   `rowSelection?: DataTableRowSelectionState`
-   `sorting?: DataTableSortingState`

### DataTableTable<T>

-   `getAllColumns: () => DataTableColumn[]`
-   `getColumn: (columnId: string) => undefined | DataTableColumn<T>`
-   `getRow: (id: string) => DataTableRow<T>`

## Get Started

---

### Defining Data Types

The `data` attribute is an array of objects that will be turned into the rows of your table. Each object in the array represents a row of data.

The first step is to define a type for the shape of your data. This type is used as a generic type for all of the other table, column, row, and cell instances.

For example, if we have a table that displays a list of person:

```typescript
type Person = {
  firstName: string;
  lastName: string;
  age: number;
  email: string;
  role: string;
}
const data: Person[] = useMemo(() => [...], []);
```

### Defining Columns

Column definitions are a very important part of building the table as they're responsible for:

-   building the underlying data model that will be used for everything including sorting, pinning, ...
-   formatting the data model into what will be displayed in the table.
-   creating columns for display-only purposes, eg. action buttons, checkboxes, ...

```typescript
const columns: DataTableColumnDef<Person>[] = [
  { id: 'firstName', header: 'First Name', accessorKey: 'firstName' },
  { id: 'lastName', header: 'Last Name', accessorKey: 'lastName' },
  { id: 'age', header: 'Age', accessorKey: 'age' },
  { id: 'email', header: 'Email', accessorKey: 'email' },
  { id: 'role', header: 'Role', accessorKey: 'role' },
];
```

### Data memoization

The data array and the column definition that you pass to the table instance must have a stable reference in order to prevent possible infinite re-renders or other performance issue.

Ensuring this stable reference can be done in multiple ways:

-   use `useState` or `useMemo` (like in the documentation examples).
-   define the data outside of the component.
-   use a 3rd party state management library (Redux, Zustand, TanStackQuery, ...).

Ensuring your data are correctly memoized is of outmost importance to ensure DataTable behavior and performance.

```typescript
function MyComponent() {
  const columns = useMemo(() => [...], []); // ✅ GOOD
  const columns = [...]; // ❌ BAD
  const [data, setData] = useState(() => [...]); // ✅ GOOD
  const data = [...]; // ❌ BAD
  return (
    <DataTable
      columns={ columns }
      data={ data }>
      ...
    </DataTable>
  );
}
```

Be careful also when setting fallback data as, if defined inline, it will get re-created on every render.

```typescript
const fallbackData = [];
function MyComponent() {
  ...
  return (
    <DataTable
      columns={ columns }
      data={ data ?? fallbackData } // ✅ GOOD
      data={ data ?? [] } // ❌ BAD
      >
      ...
    </DataTable>
  );
}
```

## Features

---

### Controlling state

Most of the table feature will work out of the box, but you will need to manage a controlled state ou your side if you need to initialize values or react to some change events.

You can set it trough the `state` attribute and keep it in sync by passing the matching `onXxx` handler function.

Here is an example of a controlled sorted table:

### Sorting

#### Disable sorting

Column sorting is enabled by default for all columns.

If you want to disabled sorting for the whole table, you can set the DataTable `enableSorting` attribute to `false`.

If you want to disabled sorting for some specific column, you can set `enableSorting` to `false` in the columns definition.

#### Custom sorting

By default, columns will be sorted alphanumerically. You can add more control on how each column should be sorted, by defining a `sortingFn` on the column definition.

#### Controlled sorting

If you want to handle the sorting state on your own, for example to pre-sort table on first render or to update it from an external source, you can use the table `state` and the `onSortingChange` handler function.

### Pagination

Pagination is not actually handled by the DataTable, you can use the  component next to your table and update your data object on page change.

Here is an example with a dummy API:

### Column pinning

The column pinning feature allows some table columns to become sticky either on the left or right side of the table.

The `columnPinning` state takes two arrays (left and right) of column ids that will be set as sticky.

### Column visibility

The column visibility feature allows table columns to be hidden or shown dynamically.

The `columnVisibility` state is a map of column ids to boolean values. A column will be hidden if its id is present in the map and the value is `false`. If the column id is not present in the map, or the value is `true`, the column will be shown.

## Examples

---

### Default

```jsx
type Person = {
    firstName: string;
    lastName: string;
    age: number;
    email: string;
    role: string;
    uuid: string;
  };
  const sampleData: Person[] = useMemo(() => [{
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
    email: 'john.doe@example.com',
    role: 'Admin',
    uuid: '5ae49b94-9ceb-4612-a087-4079a812bb0b'
  }, {
    firstName: 'Jane',
    lastName: 'Smith',
    age: 25,
    email: 'jane.smith@example.com',
    role: 'User',
    uuid: 'fb1c391c-bd88-4b96-ba39-8ab2a95d50bd'
  }, {
    firstName: 'Bob',
    lastName: 'Johnson',
    age: 35,
    email: 'bob.johnson@example.com',
    role: 'Manager',
    uuid: 'a83a58a6-a007-47f0-b04b-83989e502171'
  }], []);
  const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
    id: 'firstName',
    header: 'First Name',
    accessorKey: 'firstName'
  }, {
    id: 'lastName',
    header: 'Last Name',
    accessorKey: 'lastName'
  }, {
    id: 'age',
    header: 'Age',
    accessorKey: 'age'
  }, {
    id: 'email',
    header: 'Email',
    accessorKey: 'email'
  }, {
    id: 'role',
    header: 'Role',
    accessorKey: 'role'
  }], []);
  return <DataTable columns={sampleColumns} data={sampleData}>
      <DataTableHead />
      <DataTableBody />
    </DataTable>;
}
```

### Empty

```jsx
type Person = {
    firstName: string;
    lastName: string;
    age: number;
    email: string;
    role: string;
    uuid: string;
  };
  const sampleData: Person[] = useMemo(() => [], []);
  const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
    id: 'firstName',
    header: 'First Name',
    accessorKey: 'firstName'
  }, {
    id: 'lastName',
    header: 'Last Name',
    accessorKey: 'lastName'
  }, {
    id: 'age',
    header: 'Age',
    accessorKey: 'age'
  }, {
    id: 'email',
    header: 'Email',
    accessorKey: 'email'
  }, {
    id: 'role',
    header: 'Role',
    accessorKey: 'role'
  }], []);
  return <DataTable columns={sampleColumns} data={sampleData}>
      <DataTableHead />
      <DataTableBody />
      <DataTableEmpty>
        Empty table data      </DataTableEmpty>
    </DataTable>;
}
```

## Recipes

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

Data Grid With Query Filter

| 
Instance ID

 | 

Location

 | 

Model

 | 

Image

 | 

Backup Logic

 | 

Running since

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

## React Components/Data Table

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `columns` | `` | Yes |  | The columns definitions. |
| `data` | `` | Yes |  | The table data to display. |
| `enableMultiRowSelection` | `` | No | true | Whether the multi row selection is enabled. |
| `enableRowSelection` | `` | No |  | Whether the row selection is enabled. |
| `enableSorting` | `` | No | true | Whether the column sorting is enabled. |
| `getRowId` | `` | No |  | By default, the component will look for an id attribute in a data item to identify each row. Using this function, you can define another attribute to use instead (like uuid). |
| `loading` | `` | No |  | Whether the table data are in a loading state. This will replace each cell with a Skeleton. |
| `manualSorting` | `` | No |  | Whether the sorting is handled outside of the table. Use this is the sorting is managed on server-side. |
| `onColumnPinningChange` | `` | No |  | Callback fired when the pinned columns changes. |
| `onColumnVisibilityChange` | `` | No |  | Callback fired when the column visibility changes. |
| `onRowSelectionChange` | `` | No |  | Callback fired when the selected rows changes. |
| `onSortingChange` | `` | No |  | Callback fired when the columns sorting changes. |
| `state` | `` | No |  | @type=DataTableState The controlled table values. |
| `stickyHeader` | `` | No |  | Whether the header should be sticky. |


## Subcomponents


### DataTableEmpty




### DataTableHead



## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    rowGap: 'var(--ods-theme-row-gap)'
  }}>
      <DataTable columns={sampleColumns} data={sampleData}>
        <DataTableHead />

        <DataTableBody />
      </DataTable>

      <DataTable columns={sampleColumns} data={[]}>
        <DataTableHead />

        <DataTableBody />

        <DataTableEmpty>
          Empty table data
        </DataTableEmpty>
      </DataTable>
    </div>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { DataTable, DataTableBody, DataTableHead } from '@ovhcloud/ods-react';
import { useMemo } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Person = {
      firstName: string;
      lastName: string;
      age: number;
      email: string;
      role: string;
      uuid: string;
    };
    const sampleData: Person[] = useMemo(() => [{
      firstName: 'John',
      lastName: 'Doe',
      age: 30,
      email: 'john.doe@example.com',
      role: 'Admin',
      uuid: '5ae49b94-9ceb-4612-a087-4079a812bb0b'
    }, {
      firstName: 'Jane',
      lastName: 'Smith',
      age: 25,
      email: 'jane.smith@example.com',
      role: 'User',
      uuid: 'fb1c391c-bd88-4b96-ba39-8ab2a95d50bd'
    }, {
      firstName: 'Bob',
      lastName: 'Johnson',
      age: 35,
      email: 'bob.johnson@example.com',
      role: 'Manager',
      uuid: 'a83a58a6-a007-47f0-b04b-83989e502171'
    }], []);
    const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName'
    }, {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName'
    }, {
      id: 'age',
      header: 'Age',
      accessorKey: 'age'
    }, {
      id: 'email',
      header: 'Email',
      accessorKey: 'email'
    }, {
      id: 'role',
      header: 'Role',
      accessorKey: 'role'
    }], []);
    return <DataTable columns={sampleColumns} data={sampleData}>
        <DataTableHead />

        <DataTableBody />
      </DataTable>;
  }
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    enableMultiRowSelection: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    enableRowSelection: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    enableSorting: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    loading: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    }
  }),
  render: arg => <DataTable {...arg} columns={sampleColumns} data={sampleData}>
      <DataTableHead />

      <DataTableBody />
    </DataTable>
}
```

### Empty

```tsx
{
  globals: {
    imports: `import { DataTable, DataTableBody, DataTableEmpty, DataTableHead } from '@ovhcloud/ods-react';
import { useMemo } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Person = {
      firstName: string;
      lastName: string;
      age: number;
      email: string;
      role: string;
      uuid: string;
    };
    const sampleData: Person[] = useMemo(() => [], []);
    const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName'
    }, {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName'
    }, {
      id: 'age',
      header: 'Age',
      accessorKey: 'age'
    }, {
      id: 'email',
      header: 'Email',
      accessorKey: 'email'
    }, {
      id: 'role',
      header: 'Role',
      accessorKey: 'role'
    }], []);
    return <DataTable columns={sampleColumns} data={sampleData}>
        <DataTableHead />

        <DataTableBody />

        <DataTableEmpty>
          Empty table data
        </DataTableEmpty>
      </DataTable>;
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <DataTable columns={sampleColumns} data={sampleData}>
      <DataTableHead />

      <DataTableBody />
    </DataTable>
}
```

### Pagination Example

```tsx
{
  globals: {
    imports: `import { DataTable, DataTableBody, DataTableHead, PAGINATION_PER_PAGE, Pagination, TEXT_PRESET, Text } from '@ovhcloud/ods-react';
import { useCallback, useEffect, useMemo, useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Person = {
      firstName: string;
      lastName: string;
      age: number;
      email: string;
      role: string;
    };
    const PAGE_LIMIT = PAGINATION_PER_PAGE.option_10;
    const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName'
    }, {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName'
    }, {
      id: 'age',
      header: 'Age',
      accessorKey: 'age'
    }, {
      id: 'email',
      header: 'Email',
      accessorKey: 'email'
    }, {
      id: 'role',
      header: 'Role',
      accessorKey: 'role'
    }], []);
    const [data, setData] = useState<Person[]>(Array(PAGE_LIMIT).fill({}));
    const [page, setPage] = useState(0);
    const [total, setTotal] = useState(0);
    const fetchData = useCallback(async (page: number) => {
      return fetch(`https://dummyjson.com/users?limit=${PAGE_LIMIT}&skip=${PAGE_LIMIT * (page - 1)}&select=firstName,lastName,age,email,role`).then(res => res.json()).then(({
        total,
        users
      }) => ({
        total,
        users
      }));
    }, []);
    useEffect(() => {
      fetchData(page).then(({
        total,
        users
      }) => {
        setData(users);
        setTotal(total);
      });
    }, [page]);
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      rowGap: '16px',
      alignItems: 'center',
      justifyContent: 'center'
    }}>
        <DataTable columns={sampleColumns} data={data}>
          <Text preset={TEXT_PRESET.caption} as="caption">Paginated Table</Text>

          <DataTableHead />

          <DataTableBody />
        </DataTable>

        <Pagination onPageChange={({
        page
      }) => setPage(page)} page={page} pageSize={PAGE_LIMIT} totalItems={total} />
      </div>;
  }
}
```

### Pinned Columns

```tsx
{
  globals: {
    imports: `import { DataTable, DataTableBody, DataTableEmpty, DataTableHead } from '@ovhcloud/ods-react';
import { useMemo } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Person = {
      firstName: string;
      lastName: string;
      age: number;
      email: string;
      role: string;
      uuid: string;
    };
    const sampleData: Person[] = useMemo(() => [{
      firstName: 'John',
      lastName: 'Doe',
      age: 30,
      email: 'john.doe@example.com',
      role: 'Admin',
      uuid: '5ae49b94-9ceb-4612-a087-4079a812bb0b'
    }, {
      firstName: 'Jane',
      lastName: 'Smith',
      age: 25,
      email: 'jane.smith@example.com',
      role: 'User',
      uuid: 'fb1c391c-bd88-4b96-ba39-8ab2a95d50bd'
    }, {
      firstName: 'Bob',
      lastName: 'Johnson',
      age: 35,
      email: 'bob.johnson@example.com',
      role: 'Manager',
      uuid: 'a83a58a6-a007-47f0-b04b-83989e502171'
    }], []);
    const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName'
    }, {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName'
    }, {
      id: 'age',
      header: 'Age',
      accessorKey: 'age'
    }, {
      id: 'email',
      header: 'Email',
      accessorKey: 'email'
    }, {
      id: 'role',
      header: 'Role',
      accessorKey: 'role'
    }], []);
    const pinningState = useMemo(() => ({
      left: ['age'],
      right: ['email']
    }), []);
    return <DataTable columns={sampleColumns} data={sampleData} state={{
      columnPinning: pinningState
    }} style={{
      width: '1200px'
    }}>
        <DataTableHead />

        <DataTableBody />

        <DataTableEmpty>
          Empty table data
        </DataTableEmpty>
      </DataTable>;
  }
}
```

### Sorting Controlled

```tsx
{
  globals: {
    imports: `import { DataTable, DataTableBody, DataTableHead, TEXT_PRESET, Text } from '@ovhcloud/ods-react';
import { useMemo, useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Person = {
      firstName: string;
      lastName: string;
      age: number;
      email: string;
      role: string;
      uuid: string;
    };
    const sampleData: Person[] = useMemo(() => [{
      firstName: 'John',
      lastName: 'Doe',
      age: 30,
      email: 'john.doe@domain2.com',
      role: 'Admin',
      uuid: '5ae49b94-9ceb-4612-a087-4079a812bb0b'
    }, {
      firstName: 'Jane',
      lastName: 'Smith',
      age: 25,
      email: 'jane.smith@domain1.com',
      role: 'User',
      uuid: 'fb1c391c-bd88-4b96-ba39-8ab2a95d50bd'
    }, {
      firstName: 'Bob',
      lastName: 'Johnson',
      age: 35,
      email: 'bob.johnson@domain3.com',
      role: 'Manager',
      uuid: 'a83a58a6-a007-47f0-b04b-83989e502171'
    }], []);
    const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName'
    }, {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName'
    }, {
      id: 'age',
      header: 'Age',
      accessorKey: 'age'
    }, {
      id: 'email',
      header: 'Email',
      accessorKey: 'email'
    }, {
      id: 'role',
      header: 'Role',
      accessorKey: 'role'
    }], []);
    const [sortingState, setSortingState] = useState<DataTableSortingState>([{
      id: 'role',
      desc: true
    }, {
      id: 'lastName',
      desc: true
    }]);
    return <DataTable columns={sampleColumns} data={sampleData} onSortingChange={setSortingState} state={{
      sorting: sortingState
    }}>
        <Text preset={TEXT_PRESET.caption} as="caption">Controlled sorting</Text>

        <DataTableHead />

        <DataTableBody />
      </DataTable>;
  }
}
```

### Sorting Custom Function

```tsx
{
  globals: {
    imports: `import { DataTable, DataTableBody, DataTableHead, TEXT_PRESET, Text } from '@ovhcloud/ods-react';
import { useMemo } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Person = {
      firstName: string;
      lastName: string;
      age: number;
      email: string;
      role: string;
      uuid: string;
    };
    const sampleData: Person[] = useMemo(() => [{
      firstName: 'John',
      lastName: 'Doe',
      age: 30,
      email: 'john.doe@domain2.com',
      role: 'Admin',
      uuid: '5ae49b94-9ceb-4612-a087-4079a812bb0b'
    }, {
      firstName: 'Jane',
      lastName: 'Smith',
      age: 25,
      email: 'jane.smith@domain1.com',
      role: 'User',
      uuid: 'fb1c391c-bd88-4b96-ba39-8ab2a95d50bd'
    }, {
      firstName: 'Bob',
      lastName: 'Johnson',
      age: 35,
      email: 'bob.johnson@domain3.com',
      role: 'Manager',
      uuid: 'a83a58a6-a007-47f0-b04b-83989e502171'
    }], []);
    const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName'
    }, {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName'
    }, {
      id: 'age',
      header: 'Age',
      accessorKey: 'age'
    }, {
      id: 'email',
      header: 'Email',
      accessorKey: 'email',
      sortingFn: (rowA, rowB, columnId) => {
        const valueA = rowA.getValue<string>(columnId).split('@')[1].split('.')[0];
        const valueB = rowB.getValue<string>(columnId).split('@')[1].split('.')[0];
        return valueA.localeCompare(valueB);
      }
    }, {
      id: 'role',
      header: 'Role',
      accessorKey: 'role'
    }], []);
    return <DataTable columns={sampleColumns} data={sampleData}>
        <Text preset={TEXT_PRESET.caption} as="caption">Email sorted by domain</Text>

        <DataTableHead />

        <DataTableBody />
      </DataTable>;
  }
}
```

### Sorting Disabled Columns

```tsx
{
  globals: {
    imports: `import { DataTable, DataTableBody, DataTableHead, TEXT_PRESET, Text } from '@ovhcloud/ods-react';
import { useMemo } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Person = {
      firstName: string;
      lastName: string;
      age: number;
      email: string;
      role: string;
      uuid: string;
    };
    const sampleData: Person[] = useMemo(() => [{
      firstName: 'John',
      lastName: 'Doe',
      age: 30,
      email: 'john.doe@domain2.com',
      role: 'Admin',
      uuid: '5ae49b94-9ceb-4612-a087-4079a812bb0b'
    }, {
      firstName: 'Jane',
      lastName: 'Smith',
      age: 25,
      email: 'jane.smith@domain1.com',
      role: 'User',
      uuid: 'fb1c391c-bd88-4b96-ba39-8ab2a95d50bd'
    }, {
      firstName: 'Bob',
      lastName: 'Johnson',
      age: 35,
      email: 'bob.johnson@domain3.com',
      role: 'Manager',
      uuid: 'a83a58a6-a007-47f0-b04b-83989e502171'
    }], []);
    const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName',
      enableSorting: false
    }, {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName'
    }, {
      id: 'age',
      header: 'Age',
      accessorKey: 'age'
    }, {
      id: 'email',
      header: 'Email',
      accessorKey: 'email'
    }, {
      id: 'role',
      header: 'Role',
      accessorKey: 'role',
      enableSorting: false
    }], []);
    return <DataTable columns={sampleColumns} data={sampleData}>
        <Text preset={TEXT_PRESET.caption} as="caption">Sort disabled on some columns</Text>

        <DataTableHead />

        <DataTableBody />
      </DataTable>;
  }
}
```

### State Controlled

```tsx
{
  globals: {
    imports: `import { DataTable, DataTableBody, DataTableHead, TEXT_PRESET, Text } from '@ovhcloud/ods-react';
import { useMemo, useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Person = {
      firstName: string;
      lastName: string;
      age: number;
      email: string;
      role: string;
      uuid: string;
    };
    const sampleData: Person[] = useMemo(() => [{
      firstName: 'John',
      lastName: 'Doe',
      age: 30,
      email: 'john.doe@domain2.com',
      role: 'Admin',
      uuid: '5ae49b94-9ceb-4612-a087-4079a812bb0b'
    }, {
      firstName: 'Jane',
      lastName: 'Smith',
      age: 25,
      email: 'jane.smith@domain1.com',
      role: 'User',
      uuid: 'fb1c391c-bd88-4b96-ba39-8ab2a95d50bd'
    }, {
      firstName: 'Bob',
      lastName: 'Johnson',
      age: 35,
      email: 'bob.johnson@domain3.com',
      role: 'Manager',
      uuid: 'a83a58a6-a007-47f0-b04b-83989e502171'
    }], []);
    const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName'
    }, {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName'
    }, {
      id: 'age',
      header: 'Age',
      accessorKey: 'age'
    }, {
      id: 'email',
      header: 'Email',
      accessorKey: 'email'
    }, {
      id: 'role',
      header: 'Role',
      accessorKey: 'role'
    }], []);
    const [sortingState, setSortingState] = useState([{
      id: 'role',
      desc: true
    }]);
    return <DataTable columns={sampleColumns} data={sampleData} onSortingChange={setSortingState} state={{
      sorting: sortingState
    }}>
        <Text preset={TEXT_PRESET.caption} as="caption">Controlled sorted table</Text>

        <DataTableHead />

        <DataTableBody />
      </DataTable>;
  }
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <DataTable columns={sampleColumns} data={sampleData}>
      <DataTableHead />

      <DataTableBody />
    </DataTable>
}
```

### Visibility Example

```tsx
{
  globals: {
    imports: `import { DataTable, DataTableBody, DataTableHead, TEXT_PRESET, Text } from '@ovhcloud/ods-react';
import { useMemo, useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Person = {
      firstName: string;
      lastName: string;
      age: number;
      email: string;
      role: string;
      uuid: string;
    };
    const sampleData: Person[] = useMemo(() => [{
      firstName: 'John',
      lastName: 'Doe',
      age: 30,
      email: 'john.doe@domain2.com',
      role: 'Admin',
      uuid: '5ae49b94-9ceb-4612-a087-4079a812bb0b'
    }, {
      firstName: 'Jane',
      lastName: 'Smith',
      age: 25,
      email: 'jane.smith@domain1.com',
      role: 'User',
      uuid: 'fb1c391c-bd88-4b96-ba39-8ab2a95d50bd'
    }, {
      firstName: 'Bob',
      lastName: 'Johnson',
      age: 35,
      email: 'bob.johnson@domain3.com',
      role: 'Manager',
      uuid: 'a83a58a6-a007-47f0-b04b-83989e502171'
    }], []);
    const sampleColumns: DataTableColumnDef<Person>[] = useMemo(() => [{
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName'
    }, {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName'
    }, {
      id: 'age',
      header: 'Age',
      accessorKey: 'age'
    }, {
      id: 'email',
      header: 'Email',
      accessorKey: 'email'
    }, {
      id: 'role',
      header: 'Role',
      accessorKey: 'role'
    }], []);
    const visibileColumn = useMemo(() => ({
      age: false
    }), []);
    return <DataTable columns={sampleColumns} data={sampleData} state={{
      columnVisibility: visibileColumn
    }}>
        <Text preset={TEXT_PRESET.caption} as="caption">Age column hidden</Text>

        <DataTableHead />

        <DataTableBody />
      </DataTable>;
  }
}
```

## React Components

# Datepicker

_A **Datepicker** component is used to allow users to select a date. They can navigate through days, months and years._

## Overview

---

The Datepicker component is used for selecting dates in forms and applications. It provides a user-friendly interface for choosing dates, ensuring that the date format is consistent and valid. This component can include features such as disabled dates and custom formats.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Datepicker</td></tr><tr><th scope="row">Also known as</th><td>Calendar</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=26-7958" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/datepicker" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-datepicker--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

**Datepicker** is used to **allow a selection of a specific date** in the near future or even past. It is **useful for scheduling, or defining user dates**.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Datepicker when users need to select a specific date |
| - Choose a relevant default date when opening the calendar (e.g., default to the current year/month when not contextually set) |
| - Add a label alongside the input to clearly indicate the expected format, especially when the placeholder disappears on input |
| - Use a Datepicker when users need to select a single date or range of dates |
| - Use a Datepicker for recent or near future dates |
| - When the user has to pick dates in the distant past or future, do choose a more suitable day as Datepicker default date when it will open |

| ❌ Don't |
| --- |
| - Don't rely only on placeholders to communicate the date format. Users need persistent guidance (via a label or hint) |
| - Don't use a Datepicker when the user must select a date very far in the past or future (e.g., birthdate). Use a more accessible method like dropdowns or manual input |
| - Don't overload the interface with too many calendar options at once |
| - Don't default the calendar to an irrelevant month/year |

### Best Practices in Context

1.  **Datepicker**
2.  **Date field**
3.  **Icon button**
4.  **Dropdown calendar**
5.  **Month/Year button**
6.  **Previous/Next month buttons**
7.  **Week days**
8.  **Day**
9.  **Selected day**

## Placement

---

A **Datepicker** can be used in a page as long as there is a need to allow users to pick a date.

The date field has a fixed width by default but when used in a form its width should match the other inputs.

The **Datepicker** dropdown has a fixed width and is not adjustable.

## Behavior

---

**Opening the Datepicker**

By clicking on the **Datepicker** icon button.

**Closing the Datepicker**

-   By clicking outside the **Datepicker** dropdown
-   By selecting a date

**Date picking mode**

Navigating to previous/next month : By clicking on arrow icon  to navigate through months

Selecting a month : By clicking on the month selection  to switch to month picking mode

Picking a date : By clicking on a date

**Month picking mode**

Navigating to previous/next year : By clicking on arrow icon  to navigate through years

Selecting a year : By clicking on the month selection  to switch to year picking mode

Picking a month: By clicking on a month, the dropdown goes back to date picking mode

**Year picking mode**

Navigating to previous/next range of years : By clicking on arrow icon  to navigate through years

Selecting a decade : By clicking on the month selection  to switch to year picking mode

Picking a month: By clicking on a month, the dropdown goes back to date picking mode

**Locale**

Locale determines how the week days will be displayed according to the localization. The **Datepicker** supports the following locales: English (default), German, Spanish, French, Italian, Dutch, Polish and Portuguese.

## Navigation

---

### Focus Management

The **Datepicker** icon button can receive the focus.

When the dropdown calendar opens, the focus is moved to the currently selected date, or to today's date if none is selected.

When the dropdown is closed (via Escape or blur), focus returns to the Datepicker trigger (the input field).

### General Keyboard Shortcuts

Pressing Escape closes the Datepicker dropdown without selecting a date.

Pressing Tab or Shift + Tab moves focus through dropdown controls when open.

### Navigating days

-   Arrow Left: Move to the previous day
-   Arrow Right: Move to the next day
-   Arrow Up: Move to the same weekday of the previous week
-   Arrow Down: Move to the same weekday of the next week

### Jumping within the calendar

-   Home: Move to the first day of the current month
-   End: Move to the last day of the current month
-   Page Up: Move to the same date of the previous month
-   Page Down: Move to the same date of the next month

### Selecting a date or changing view

-   Enter: Select the currently focused date and close the dropdown
-   Typing a valid date format (e.g., yyyy-mm-dd) in the input and pressing Enter will also select the date

### Navigating to month/year view

-   Ctrl / Cmd + Arrow Up (first press): Switches to month selection view
-   Ctrl / Cmd + Arrow Up (second press): Switches to year selection view
-   Use Arrow keys to move within month/year grids
-   Press Enter to confirm selection and return to the previous view
-   Ctrl / Cmd + Arrow Left / Arrow Right: Quickly navigate to the previous or next month from date view

## Accessibility

---

To ensure proper accessibility, the **Datepicker** must be correctly labeled, provide clear guidance on date format and meaningful context when a clearable button is used.

### Always provide an explicit label

Every **Datepicker** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose using either **FormField** or a native label tag.

```jsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Start date:      </FormFieldLabel>
      <Datepicker>
        <DatepickerControl />
        <DatepickerContent />
      </Datepicker>
    </FormField>
}
```

Screen readers will announce the label, the field and its content.

### Provide guidance on date format

Since screen readers can only interact with the **Datepicker** input field, it’s important to provide guidance on the expected date format to ensure correct input.

```jsx
{
  globals: {
    imports: `import { TEXT_PRESET, Datepicker, DatepickerContent, DatepickerControl, FormField, FormFieldHelper, FormFieldLabel, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Start date:      </FormFieldLabel>
      <Datepicker>
        <DatepickerControl placeholder="DD-MM-YYYY" />
        <DatepickerContent />
      </Datepicker>
      <FormFieldHelper>
        <Text preset={TEXT_PRESET.caption}>
          Expected format: DD-MM-YYYY        </Text>
      </FormFieldHelper>
    </FormField>
}
```

Screen readers will announce the label, the field, its content and the helper.

Start date:

```jsx
<FormField>
  <FormFieldLabel>
    Start date:  </FormFieldLabel>
  <Datepicker i18n={{
  [INPUT_I18N.clearButton]: 'Clear date'
}}>
    <DatepickerControl clearable />
    <DatepickerContent />
  </Datepicker>
</FormField>
```

Screen readers will announce the label, the field, its content and custom label of focused action.

## React Components

# Datepicker

## Overview

---

## Anatomy

---

Datepicker

DatepickerContent

DatepickerControl

---

| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
| --- | --- | --- | --- | --- | --- | --- |
|  |  |  |  |  |  |  |
|  |  |  |  |  |  |  |
|  |  |  |  |  |  |  |
|  |  |  |  |  |  |  |
|  |  |  |  |  |  |  |
|  |  |  |  |  |  |  |

<table data-scope="date-picker" data-part="table" role="grid" data-columns="4" aria-roledescription="calendar year" id="datepicker::rb::table:month :re:" data-view="month" dir="ltr" tabindex="-1" class="_datepicker-content__table_i233a_13"><tbody data-scope="date-picker" data-part="table-body" data-view="month"><tr data-scope="date-picker" data-part="table-row" data-view="month"><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="1"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:1" aria-label="January 2026" data-view="month" data-value="1" tabindex="-1" type="button">Jan</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2" aria-label="February 2026" data-view="month" data-value="2" tabindex="-1" type="button">Feb</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="3"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:3" aria-label="March 2026" data-view="month" data-value="3" tabindex="-1" type="button">Mar</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" aria-selected="true" data-selected="" data-value="4"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:4" data-selected="" data-focus="" aria-label="April 2026" data-view="month" data-value="4" tabindex="0" type="button">Apr</button></td></tr><tr data-scope="date-picker" data-part="table-row" data-view="month"><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="5"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:5" aria-label="May 2026" data-view="month" data-value="5" tabindex="-1" type="button">May</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="6"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:6" aria-label="June 2026" data-view="month" data-value="6" tabindex="-1" type="button">Jun</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="7"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:7" aria-label="July 2026" data-view="month" data-value="7" tabindex="-1" type="button">Jul</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="8"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:8" aria-label="August 2026" data-view="month" data-value="8" tabindex="-1" type="button">Aug</button></td></tr><tr data-scope="date-picker" data-part="table-row" data-view="month"><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="9"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:9" aria-label="September 2026" data-view="month" data-value="9" tabindex="-1" type="button">Sep</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="10"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:10" aria-label="October 2026" data-view="month" data-value="10" tabindex="-1" type="button">Oct</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="11"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:11" aria-label="November 2026" data-view="month" data-value="11" tabindex="-1" type="button">Nov</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="12"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__month_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:12" aria-label="December 2026" data-view="month" data-value="12" tabindex="-1" type="button">Dec</button></td></tr></tbody></table>

<table data-scope="date-picker" data-part="table" role="grid" data-columns="4" aria-roledescription="calendar decade" id="datepicker::rb::table:year :rf:" data-view="year" dir="ltr" tabindex="-1" class="_datepicker-content__table_i233a_13"><tbody data-scope="date-picker" data-part="table-body" data-view="year"><tr data-scope="date-picker" data-part="table-row" data-view="year"><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2020"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2020" aria-label="2020" data-value="2020" data-view="year" tabindex="-1" type="button">2020</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2021"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2021" aria-label="2021" data-value="2021" data-view="year" tabindex="-1" type="button">2021</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2022"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2022" aria-label="2022" data-value="2022" data-view="year" tabindex="-1" type="button">2022</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2023"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2023" aria-label="2023" data-value="2023" data-view="year" tabindex="-1" type="button">2023</button></td></tr><tr data-scope="date-picker" data-part="table-row" data-view="year"><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2024"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2024" aria-label="2024" data-value="2024" data-view="year" tabindex="-1" type="button">2024</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2025"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2025" aria-label="2025" data-value="2025" data-view="year" tabindex="-1" type="button">2025</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" aria-selected="true" data-selected="" data-value="2026"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2026" data-selected="" data-focus="" aria-label="2026" data-value="2026" data-view="year" tabindex="0" type="button">2026</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2027"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2027" aria-label="2027" data-value="2027" data-view="year" tabindex="-1" type="button">2027</button></td></tr><tr data-scope="date-picker" data-part="table-row" data-view="year"><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2028"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2028" aria-label="2028" data-value="2028" data-view="year" tabindex="-1" type="button">2028</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2029"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2029" aria-label="2029" data-value="2029" data-view="year" tabindex="-1" type="button">2029</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2030"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2030" aria-label="2030" data-outside-range="" data-value="2030" data-view="year" tabindex="-1" type="button">2030</button></td><td data-scope="date-picker" data-part="table-cell" dir="ltr" role="gridcell" data-value="2031"><button class="_button_6crpx_2 _button--primary_6crpx_276 _button--sm_6crpx_209 _button--ghost_6crpx_327 _datepicker-content__table__year_i233a_55" data-ods="button" data-scope="date-picker" data-part="table-cell-trigger" dir="ltr" role="button" id="datepicker::rb::cell-trigger:2031" aria-label="2031" data-outside-range="" data-value="2031" data-view="year" tabindex="-1" type="button">2031</button></td></tr></tbody></table>

## Datepicker

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
dateFormatter

 | `(arg: DatepickerFormatterArg) => string` | - | `undefined` | Format the date to display in the input. |
| 

defaultOpen

 | `boolean` | - | `undefined` | The initial open state of the datepicker. Use when you don't need to control the open state of the datepicker. |
| 

defaultValue

 | `Date | string` | - | `undefined` | The initial selected date. Use when you don't need to control the selected date of the datepicker. |
| 

defaultView

 | `DATEPICKER_VIEW` | - | `undefined` | The default view of the calendar (day, month, year). |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

disabledDates

 | `Date[]` | - | `undefined` | List of dates that cannot be selected. |
| 

disabledWeekDays

 | `DATEPICKER_DAY[]` | - | `undefined` | List of week days that cannot be selected. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override (see Input i18n keys). |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

locale

 | `string` | - | `undefined` | The locale to use when formatting the date. |
| 

max

 | `Date | string` | - | `undefined` | The maximum date that can be selected. |
| 

maxView

 | `DATEPICKER_VIEW` | - | `undefined` | The maximum view of the calendar (day, month, year). |
| 

min

 | `Date | string` | - | `undefined` | The minimum date that can be selected. |
| 

minView

 | `DATEPICKER_VIEW` | - | `undefined` | The minimum view of the calendar (day, month, year). |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onOpenChange

 | `(detail: DatepickerOpenChangeDetail) => void` | - | `undefined` | Callback fired when the datepicker open state changes. |
| 

onValueChange

 | `(detail: DatepickerValueChangeDetail) => void` | - | `undefined` | Callback fired when the value changes. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the datepicker. |
| 

overlayConfig

 | `object` | - | `undefined` | The overlay configuration. |
| 

flip

 | `boolean` | - | `-` | Whether to flip the position. |
| 

placeholder

 | `string` | - | `undefined` | The placeholder text to display in the input. |
| 

positionerStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |
| 

readOnly

 | `boolean` | - | `undefined` | Whether the component is readonly. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. |
| 

value

 | `Date | string | null` | - | `undefined` | The controlled selected date. |
| 

view

 | `DATEPICKER_VIEW` | - | `undefined` | The controlled view of the calendar (day, month, year). |

## DatepickerContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. |

## DatepickerControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [input attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#attributes) . |
| 
clearable

 | `boolean` | - | `undefined` | Whether the clear button is displayed. |
| 

loading

 | `boolean` | - | `undefined` | Whether the component is in loading state. |

## Enums

---

### DATEPICKER_DAY

-   friday =`"5"`
-   monday =`"1"`
-   saturday =`"6"`
-   sunday =`"0"`
-   thursday =`"4"`
-   tuesday =`"2"`
-   wednesday =`"3"`

### DATEPICKER_VIEW

-   day =`"day"`
-   month =`"month"`
-   year =`"year"`

## Interfaces

---

### DatepickerFormatterArg

-   `date: Date`
-   `locale: string`

### DatepickerOpenChangeDetail

-   `open: boolean`

### DatepickerValueChangeDetail

-   `value: null | Date`
-   `valueAsString: string | null`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-datepicker-content-padding-horizontal | calc(var(--ods-theme-padding-horizontal) / 2) | 
 |
| --ods-datepicker-content-padding-vertical | calc(var(--ods-theme-padding-vertical) / 2) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker>
      <DatepickerControl />
      <DatepickerContent />
    </Datepicker>
}
```

### Disabled

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Disabled:</p>
      <Datepicker disabled>
        <DatepickerControl />
        <DatepickerContent />
      </Datepicker>
      <p>Disabled Dates:</p>
      <Datepicker disabledDates={[new Date(Date.now() - 86400000), new Date(), new Date(Date.now() + 86400000)]}>
        <DatepickerControl />
        <DatepickerContent />
      </Datepicker>
      <p>Disabled Week Days:</p>
      <Datepicker disabledWeekDays={[0, 3]}>
        <DatepickerControl />
        <DatepickerContent />
      </Datepicker>
    </>
}
```

### Readonly

```jsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker readOnly>
      <DatepickerControl />
      <DatepickerContent />
    </Datepicker>
}
```

### Date formatter

```jsx
<Datepicker dateFormatter={({
  date
}) => `${date.getFullYear()}`} placeholder="yyyy">
    <DatepickerControl />
    <DatepickerContent />
  </Datepicker>
```

### Max / Min

```jsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker max={new Date(Date.now() + 86400000 * 10)} min={new Date(Date.now() - 86400000 * 10)}>
      <DatepickerControl />
      <DatepickerContent />
    </Datepicker>
}
```

### Max view

```jsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker maxView="day">
      <DatepickerControl />
      <DatepickerContent />
    </Datepicker>
}
```

### Min view

```jsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker minView="month">
      <DatepickerControl />
      <DatepickerContent />
    </Datepicker>
}
```

### Form field

```jsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Label:      </FormFieldLabel>
      <Datepicker>
        <DatepickerControl />
        <DatepickerContent />
      </Datepicker>
    </FormField>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Datepicker

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `dateFormatter` | `` | No |  | Format the date to display in the input. |
| `defaultOpen` | `` | No |  | The initial open state of the datepicker. Use when you don't need to control the open state of the datepicker. |
| `defaultValue` | `` | No |  | The initial selected date. Use when you don't need to control the selected date of the datepicker. |
| `defaultView` | `` | No |  | The default view of the calendar (day, month, year). |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `disabledDates` | `` | No |  | List of dates that cannot be selected. |
| `disabledWeekDays` | `` | No |  | List of week days that cannot be selected. |
| `i18n` | `` | No |  | Internal translations override (see Input i18n keys). |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `locale` | `` | No |  | The locale to use when formatting the date. |
| `max` | `` | No |  | The maximum date that can be selected. |
| `maxView` | `` | No |  | The maximum view of the calendar (day, month, year). |
| `min` | `` | No |  | The minimum date that can be selected. |
| `minView` | `` | No |  | The minimum view of the calendar (day, month, year). |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onOpenChange` | `` | No |  | Callback fired when the datepicker open state changes. |
| `onValueChange` | `` | No |  | Callback fired when the value changes. |
| `open` | `` | No |  | The controlled open state of the datepicker. |
| `overlayConfig` | `` | No |  | The overlay configuration. |
| `placeholder` | `` | No |  | The placeholder text to display in the input. |
| `positionerStyle` | `` | No |  | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |
| `readOnly` | `` | No |  | Whether the component is readonly. |
| `required` | `` | No |  | Whether the component is required. |
| `value` | `` | No |  | The controlled selected date. |
| `view` | `` | No |  | The controlled view of the calendar (day, month, year). |


## Subcomponents


### DatepickerContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. |



### DatepickerControl



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `clearable` | `` | No |  | Whether the clear button is displayed. |
| `loading` | `` | No |  | Whether the component is in loading state. |


## Examples


### Accessibility Date Format

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, Datepicker, DatepickerContent, DatepickerControl, FormField, FormFieldHelper, FormFieldLabel, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Start date:
      </FormFieldLabel>

      <Datepicker>
        <DatepickerControl placeholder="DD-MM-YYYY" />

        <DatepickerContent />
      </Datepicker>

      <FormFieldHelper>
        <Text preset={TEXT_PRESET.caption}>
          Expected format: DD-MM-YYYY
        </Text>
      </FormFieldHelper>
    </FormField>
}
```

### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Start date:
      </FormFieldLabel>

      <Datepicker>
        <DatepickerControl />

        <DatepickerContent />
      </Datepicker>
    </FormField>
}
```

### Accessibility I 18 N

```tsx
{
  globals: {
    imports: `import { INPUT_I18N, Datepicker, DatepickerContent, DatepickerControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => <FormField>
      <FormFieldLabel>
        Start date:
      </FormFieldLabel>

      <Datepicker i18n={{
      [INPUT_I18N.clearButton]: 'Clear date'
    }}>
        <DatepickerControl clearable />

        <DatepickerContent />
      </Datepicker>
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  parameters: {
    layout: 'start'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    height: '330px'
  }}>
      <Datepicker defaultValue={new Date()} open overlayConfig={{
      flip: false
    }}>
        <DatepickerControl />

        <DatepickerContent createPortal={false} />
      </Datepicker>
    </div>
}
```

### Date Formatter

```tsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => <Datepicker dateFormatter={({
    date
  }) => `${date.getFullYear()}`} placeholder="yyyy">
      <DatepickerControl />

      <DatepickerContent />
    </Datepicker>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker>
      <DatepickerControl />

      <DatepickerContent />
    </Datepicker>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Datepicker disabled={arg.disabled} invalid={arg.invalid} locale={arg.locale} placeholder={arg.placeholder} readOnly={arg.readOnly}>
      <DatepickerControl clearable={arg.clearable} loading={arg.loading} />

      <DatepickerContent />
    </Datepicker>,
  argTypes: orderControls({
    clearable: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: {
        type: 'boolean'
      }
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    loading: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: {
        type: 'boolean'
      }
    },
    locale: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'iso code'
        }
      },
      control: {
        type: 'select'
      },
      options: ['de', 'en', 'es', 'fr', 'it', 'nl', 'pl', 'pt']
    },
    placeholder: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    }
  })
}
```

### Disabled

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Disabled:</p>

      <Datepicker disabled>
        <DatepickerControl />

        <DatepickerContent />
      </Datepicker>

      <p>Disabled Dates:</p>

      <Datepicker disabledDates={[new Date(Date.now() - 86400000), new Date(), new Date(Date.now() + 86400000)]}>
        <DatepickerControl />

        <DatepickerContent />
      </Datepicker>

      <p>Disabled Week Days:</p>

      <Datepicker disabledWeekDays={[0, 3]}>
        <DatepickerControl />

        <DatepickerContent />
      </Datepicker>
    </>
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl, FormField, FormFieldLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Label:
      </FormFieldLabel>

      <Datepicker>
        <DatepickerControl />

        <DatepickerContent />
      </Datepicker>
    </FormField>
}
```

### Max View

```tsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker maxView="day">
      <DatepickerControl />

      <DatepickerContent />
    </Datepicker>
}
```

### Min Max

```tsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker max={new Date(Date.now() + 86400000 * 10)} min={new Date(Date.now() - 86400000 * 10)}>
      <DatepickerControl />

      <DatepickerContent />
    </Datepicker>
}
```

### Min View

```tsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker minView="month">
      <DatepickerControl />

      <DatepickerContent />
    </Datepicker>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Datepicker defaultValue={new Date()}>
      <DatepickerControl />

      <DatepickerContent />
    </Datepicker>
}
```

### Readonly

```tsx
{
  globals: {
    imports: `import { Datepicker, DatepickerContent, DatepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Datepicker readOnly>
      <DatepickerControl />

      <DatepickerContent />
    </Datepicker>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px'
  }}>
      <Datepicker>
        <DatepickerControl placeholder="Default" />
        <DatepickerContent createPortal={false} />
      </Datepicker>

      <Datepicker>
        <DatepickerControl loading placeholder="Loading" />
        <DatepickerContent createPortal={false} />
      </Datepicker>

      <Datepicker>
        <DatepickerControl clearable placeholder="Clearable" />
        <DatepickerContent createPortal={false} />
      </Datepicker>

      <Datepicker invalid>
        <DatepickerControl placeholder="Invalid" />
        <DatepickerContent createPortal={false} />
      </Datepicker>

      <Datepicker disabled>
        <DatepickerControl placeholder="Disabled" />
        <DatepickerContent createPortal={false} />
      </Datepicker>

      <Datepicker readOnly>
        <DatepickerControl placeholder="Read only" />
        <DatepickerContent createPortal={false} />
      </Datepicker>
    </div>
}
```

## React Components

# Divider

_A **Divider** component is a spacer used to add white space between two elements on a page._

---

## Overview

---

A **Divider** is a UI component used to separate content into clear, distinct sections, providing visual breaks between different elements or groups of content.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Divider</td></tr><tr><th scope="row">Also known as</th><td>Separator, Spacer, Horizontal Line</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=26-19845" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/divider" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-divider--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Divider** is used to separate content on a page.

It brings clarity to a layout by dividing content in proximity.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Divider to visually separate content into meaningful sections or groups |
| - Place a Divider below section titles or headers to emphasize hierarchy and structure |
| - Use Divider to indicate content boundaries without relying solely on spacing or background changes |
| - Keep Divider usage subtle and consistent across the layout to preserve clarity |

| ❌ Don't |
| --- |
| - Don't place a Divider above section titles or headers, it breaks natural reading flow |
| - Don't overuse Dividers on a single page, too many can create visual noise and clutter |
| - Don't use Divider as a primary layout mechanism, prefer spacing, grouping, or cards for structure |
| - Don't rely on Divider to fix unclear content grouping, improve the layout or hierarchy instead |
| - Don't use Divider when spacing alone is sufficient to separate elements |
| - Don't apply inconsistent styling (e.g., thickness, color) that breaks the visual consistency of the interface |

### Best Practices in Context

1.  **Divider**
2.  **Spacing**

## Placement

---

Use **Divider** between closely related content blocks or UI elements to enhance readability without adding visual noise.

It works well in layouts like sidebars, menus, or cards to separate sections clearly.

## Variation

---

### Spacing

It can be adjusted around the **Divider** to match the layout and visual rhythm of the content.

### Color

-   **Light** (default): used on dark or colored backgrounds to maintain contrast
-   **Dark**: used on light backgrounds for clear, subtle separation

### Orientation

-   **Horizontal** (default): separates content stacked vertically (for example, between sections of a page)
-   **Vertical**: separates content arranged horizontally (for example, between items in a toolbar, breadcrumbs, inline actions, or columns)

## Navigation

---

The **Divider** component is purely visual and does not receive keyboard focus. It serves as a separator between content sections and does not impact keyboard navigation.

## Accessibility

---

The **Divider** provides a clear visual separation between content sections which can help users with visual impairments to better understand the layout and structure of the page.

## React Components

# Divider

## Overview

---

## Anatomy

---

Divider

---

## Divider

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [hr attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/hr#attributes) . |
| 
colorDeprecated

 | `DIVIDER_COLOR` | - | `undefined` | The color preset to use. DEPRECATED: Color will now always be primary, if you need another color, prefer overriding it using css. |
| 

orientation

 | `'horizontal' | 'vertical'` | - | `DIVIDER_ORIENTATION.horizontal` | The orientation of the divider. |
| 

spacing

 | `DIVIDER_SPACING` | - | `DIVIDER_SPACING._2` | The spacing preset to use. |

## Enums

---

### DIVIDER_COLOR

Deprecated

-   neutral =`"neutral"`
-   primary =`"primary"`

### DIVIDER_ORIENTATION

-   horizontal =`"horizontal"`
-   vertical =`"vertical"`

### DIVIDER_SPACING

-   _0 =`"0"`
-   _12 =`"12"`
-   _16 =`"16"`
-   _2 =`"2"`
-   _24 =`"24"`
-   _32 =`"32"`
-   _4 =`"4"`
-   _40 =`"40"`
-   _48 =`"48"`
-   _6 =`"6"`
-   _64 =`"64"`
-   _8 =`"8"`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Divider } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Divider />
}
```

### Color

```jsx
{
  globals: {
    imports: `import { DIVIDER_COLOR, Divider } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Divider color={DIVIDER_COLOR.neutral} />
      <Divider color={DIVIDER_COLOR.primary} />
    </>
}
```

### Orientation

```jsx
{
  globals: {
    imports: `import { DIVIDER_ORIENTATION, Divider } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Divider orientation={DIVIDER_ORIENTATION.horizontal} />
      <Divider orientation={DIVIDER_ORIENTATION.vertical} />
    </>
}
```

### Spacing

```jsx
{
  globals: {
    imports: `import { DIVIDER_SPACING, Divider } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Divider spacing={DIVIDER_SPACING._0} />
      <Divider spacing={DIVIDER_SPACING._2} />
      <Divider spacing={DIVIDER_SPACING._4} />
      <Divider spacing={DIVIDER_SPACING._6} />
      <Divider spacing={DIVIDER_SPACING._8} />
      <Divider spacing={DIVIDER_SPACING._12} />
      <Divider spacing={DIVIDER_SPACING._16} />
      <Divider spacing={DIVIDER_SPACING._24} />
      <Divider spacing={DIVIDER_SPACING._32} />
      <Divider spacing={DIVIDER_SPACING._40} />
      <Divider spacing={DIVIDER_SPACING._48} />
      <Divider spacing={DIVIDER_SPACING._64} />
    </>
}
```

## Recipes

---

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

## React Components/Divider

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No |  | @deprecated The color preset to use. DEPRECATED: Color will now always be primary, if you need another color, prefer overriding it using css. |
| `orientation` | `` | No | DIVIDER_ORIENTATION.horizontal | The orientation of the divider. |
| `spacing` | `` | No | DIVIDER_SPACING._2 | The spacing preset to use. |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
      <Divider color={DIVIDER_COLOR.primary} />
      <p>Interdum et malesuada fames ac ante ipsum primis in faucibus.</p>
    </>
}
```

### Color

```tsx
{
  globals: {
    imports: `import { DIVIDER_COLOR, Divider } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Divider color={DIVIDER_COLOR.neutral} />
      <Divider color={DIVIDER_COLOR.primary} />
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Divider } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Divider />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    color: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'DIVIDER_COLOR'
        }
      },
      control: {
        type: 'select'
      },
      options: DIVIDER_COLORS
    },
    orientation: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'DIVIDER_ORIENTATION'
        }
      },
      control: {
        type: 'select'
      },
      options: DIVIDER_ORIENTATIONS
    },
    spacing: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'DIVIDER_SPACING'
        }
      },
      control: {
        type: 'select'
      },
      options: DIVIDER_SPACINGS
    }
  })
}
```

### Orientation

```tsx
{
  globals: {
    imports: `import { DIVIDER_ORIENTATION, Divider } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Divider orientation={DIVIDER_ORIENTATION.horizontal} />
      <Divider orientation={DIVIDER_ORIENTATION.vertical} />
    </>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
      <Divider color={DIVIDER_COLOR.primary} />
      <p>Interdum et malesuada fames ac ante ipsum primis in faucibus.</p>
    </>
}
```

### Spacing

```tsx
{
  globals: {
    imports: `import { DIVIDER_SPACING, Divider } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Divider spacing={DIVIDER_SPACING._0} />
      <Divider spacing={DIVIDER_SPACING._2} />
      <Divider spacing={DIVIDER_SPACING._4} />
      <Divider spacing={DIVIDER_SPACING._6} />
      <Divider spacing={DIVIDER_SPACING._8} />
      <Divider spacing={DIVIDER_SPACING._12} />
      <Divider spacing={DIVIDER_SPACING._16} />
      <Divider spacing={DIVIDER_SPACING._24} />
      <Divider spacing={DIVIDER_SPACING._32} />
      <Divider spacing={DIVIDER_SPACING._40} />
      <Divider spacing={DIVIDER_SPACING._48} />
      <Divider spacing={DIVIDER_SPACING._64} />
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <Divider />
}
```

## React Components

# Drawer

The Drawer component provides a sliding panel that reveals additional content without navigating away from the current page.

## Overview

---

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Drawer</td></tr><tr><th scope="row">Also known as</th><td>Sidenav, Panel</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/5BA2ZwZaX2bzfiWwTyMpQH/ODS---UI-Kit?node-id=5488-15121&amp;p=f&amp;t=gPfKLQM0iEuF2FXm-0" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/drawer" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-drawer--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Drawer** component is ideal for use cases where content should be revealed in context without navigating to a new page. Common examples include:

-   Side navigation menus
-   Filters and settings panels
-   Detail views or contextual information

| ✅ Do |
| --- |
| - Use a Drawer to display contextual content or actions that stay relevant to the current page (e.g., filters, side forms, settings) |
| - Allow the Drawer to be dismissed via clear and accessible interactions (e.g., close button, escape key, clicking outside) |
| - Customize the Drawer's width, height, and placement to match your layout and content needs |
| - Keep transitions smooth and responsive, respecting the expected animation duration (e.g., 300ms) in the OVHcloud Design System |
| - Maintain proper layering and z-index behavior |
| - Use Drawers to maintain continuity of the user's workflow without breaking the page context |

| ❌ Don't |
| --- |
| - Don't add more than one open Drawer at a time |
| - Don't use excessively large Drawers that cover most or all of the page |
| - Don't rely on Drawers for critical actions that require user confirmation, Modals are better suited for that |
| - Don't leave Drawers open without a visible way to close them, always provide a clear exit path |

### Best Practices in Context

1.  **Drawer**

## Placement

---

The **Drawer** can be positioned on any edge of the screen:

-   **Left/Right**: Covers 100% of the page height, with a default width of 320px and a maximum width of 100% - 3rem (48px)
-   **Top/Bottom**: Covers 100% of the page width, with a default height of 320px and a maximum height of 100vh - 3rem (48px)

The **Drawer** is displayed above all page content but is positioned below the Modal in terms of layering, following the z-index hierarchy defined by OVHcloud Design System. This ensures consistent stacking behavior across components.

By default, the OVHcloud Design System does not enforce whether the **Drawer** is fixed or scrollable. Integrators can configure this behavior based on their specific use case and requirements.

Be careful the placement of the drawer can be broken if you create a new [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context) . For example with a transform style or scale.

## Behavior

---

The **Drawer** supports the following interactions:

-   Expansion: Triggered by interacting with a designated element, such as a button.
-   Collapsing:
-   By interacting with the same trigger used for expansion
-   By pressing the escape key

Animation: The **Drawer** slides into view with a smooth animation (default duration: 300ms).

## Variation

---

The **Drawer** can slide in from the left, right, top, or bottom, depending on its configuration and the layout of the page.

## Navigation

---

### Focus Management

When the **Drawer** is opened, focus automatically moves to the first focusable element inside.

When the **Drawer** is closed, focus returns to the trigger element unless overridden by the integrator.

### General Keyboard Shortcuts

Pressing Escape closes the **Drawer**.

Pressing Tab moves focus forward through the focusable elements inside the **Drawer**.

Pressing Shift + Tab moves focus backward within the **Drawer**.

Once the last focusable element is reached, focus does not leave the **Drawer** unless it is closed.

## Accessibility

---

To ensure the **Drawer** is fully accessible, correct ARIA attributes must be implemented. This ensures screen readers announce the **Drawer** properly and that keyboard users can interact with it effectively.

### Linking the Drawer title and content

Ensure that assistive technologies announce the **Drawer** correctly using [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby) or [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) , and [aria-describedby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-describedby) .

```jsx
<Drawer>
  <DrawerTrigger asChild>
    <Button>
      Trigger Drawer    </Button>
  </DrawerTrigger>
  <DrawerContent
    aria-describedby="drawer-content"
    aria-label="My drawer"
  >
    <DrawerBody id="drawer-content">
      The drawer content    </DrawerBody>
  </DrawerContent>
</Drawer>
```

```jsx
<Drawer>
  <DrawerTrigger asChild>
    <Button>
      Trigger Drawer    </Button>
  </DrawerTrigger>
  <DrawerContent
    aria-describedby="drawer-content"
    aria-labelledby="drawer-title"
  >
    <DrawerBody>
      <h2 id="drawer-title">
        My drawer      </h2>
      <p id="drawer-content">
        The drawer content      </p>
    </DrawerBody>
  </DrawerContent>
</Drawer>
```

Screen readers will announce the sections referenced by the aria attributes.

## React Components

# Drawer

## Overview

---

## Anatomy

---

Drawer

DrawerBody

DrawerContent

DrawerTrigger

---

## Drawer

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
closeOnEscape

 | `boolean` | - | `true` | Whether to close the drawer when the escape key is pressed. |
| 

closeOnInteractOutside

 | `boolean` | - | `false` | Whether to close the drawer when the outside is clicked. |
| 

defaultOpen

 | `boolean` | - | `undefined` | The initial open state of the drawer. Use when you don't need to control the open state of the drawer. |
| 

onOpenChange

 | `(detail: DrawerOpenChangeDetail) => void` | - | `undefined` | Callback fired when the drawer open state changes. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the drawer. |
| 

positionerStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the overlay positioner. |

## DrawerBody

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## DrawerContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. |
| 

position

 | `DRAWER_POSITION` | - | `DRAWER_POSITION.left` | The drawer position in the screen. |

## DrawerTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## Enums

---

### DRAWER_POSITION

-   bottom =`"bottom"`
-   left =`"left"`
-   right =`"right"`
-   top =`"top"`

## Interfaces

---

### DrawerOpenChangeDetail

-   `open: boolean`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-drawer-padding-horizontal | calc(var(--ods-theme-padding-horizontal) * 2) | 
 |
| --ods-drawer-padding-vertical | calc(var(--ods-theme-padding-vertical) * 2) | 

 |
| --ods-drawer-z-index | calc(calc(var(--ods-theme-overlay-z-index) + 1) + 1) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Button, Drawer, DrawerBody, DrawerContent, DrawerTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Trigger Drawer        </Button>
      </DrawerTrigger>
      <DrawerContent>
        <DrawerBody>
          My drawer content        </DrawerBody>
      </DrawerContent>
    </Drawer>
}
```

### Position

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    alignItems: 'center',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { DRAWER_POSITION, Button, Drawer, DrawerBody, DrawerContent, DrawerTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
    <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Top drawer        </Button>
      </DrawerTrigger>
      <DrawerContent position={DRAWER_POSITION.top}>
        <DrawerBody>
          Top drawer content        </DrawerBody>
      </DrawerContent>
    </Drawer>
   <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Left drawer        </Button>
      </DrawerTrigger>
      <DrawerContent position={DRAWER_POSITION.left}>
        <DrawerBody>
          Left drawer content        </DrawerBody>
      </DrawerContent>
    </Drawer>
   <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Right Drawer        </Button>
      </DrawerTrigger>
      <DrawerContent position={DRAWER_POSITION.right}>
        <DrawerBody>
          Right drawer content        </DrawerBody>
      </DrawerContent>
    </Drawer>
   <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Bottom Drawer        </Button>
      </DrawerTrigger>
      <DrawerContent position={DRAWER_POSITION.bottom}>
        <DrawerBody>
          Bottom drawer content        </DrawerBody>
      </DrawerContent>
    </Drawer>
    </>
}
```

### Overlay Elements

```jsx
{
  globals: {
    imports: `import { Button, Drawer, DrawerContent, DrawerBody, DrawerTrigger, ICON_NAME, Icon, Select, SelectContent, SelectControl, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Trigger Drawer        </Button>
      </DrawerTrigger>
      <DrawerContent>
        <DrawerBody style={{
        display: 'grid',
        columnGap: '8px',
        alignItems: 'center',
        gridTemplateColumns: '1fr max-content'
      }}>
          <Select items={[{
          label: 'Dog',
          value: 'dog'
        }, {
          label: 'Cat',
          value: 'cat'
        }, {
          label: 'Hamster',
          value: 'hamster'
        }, {
          label: 'Parrot',
          value: 'parrot'
        }, {
          label: 'Spider',
          value: 'spider'
        }, {
          label: 'Goldfish',
          value: 'goldfish'
        }]}>
            <SelectControl />
            <SelectContent createPortal={false} />
          </Select>
          <Tooltip>
            <TooltipTrigger asChild>
              <Icon name={ICON_NAME.circleQuestion} style={{
              fontSize: '24px'
            }} />
            </TooltipTrigger>
            <TooltipContent createPortal={false}>
              This is the tooltip content            </TooltipContent>
          </Tooltip>
        </DrawerBody>
      </DrawerContent>
    </Drawer>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Drawer

## Subcomponents


### DrawerBody




### DrawerContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. |
| `position` | `` | No | DRAWER_POSITION.left | The drawer position in the screen. |



### DrawerTrigger



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `asChild` | `` | No |  | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |


## Examples


### Accessibility Aria Label

```tsx
{
  globals: {
    imports: `import { Button, Drawer, DrawerBody, DrawerContent, DrawerTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Trigger Drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent aria-describedby="drawer-content" aria-label="My drawer">
        <DrawerBody id="drawer-content">
          The drawer content
        </DrawerBody>
      </DrawerContent>
    </Drawer>
}
```

### Accessibility Aria Labelled By

```tsx
{
  globals: {
    imports: `import { Button, Drawer, DrawerBody, DrawerContent, DrawerTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Trigger Drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent aria-describedby="drawer-content" aria-labelledby="drawer-title">
        <DrawerBody>
          <h2 id="drawer-title">
            My drawer
          </h2>

          <p id="drawer-content">
            The drawer content
          </p>
        </DrawerBody>
      </DrawerContent>
    </Drawer>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Drawer open>
      <DrawerTrigger asChild>
        <Button>
          Drawer trigger
        </Button>
      </DrawerTrigger>

      <DrawerContent createPortal={false} position={DRAWER_POSITION.right} style={{
      position: 'absolute',
      width: 'auto',
      height: 'auto',
      animation: 'none'
    }}>
        <DrawerBody>
          My drawer content
        </DrawerBody>
      </DrawerContent>
    </Drawer>
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { Button, Drawer, DrawerBody, DrawerContent } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [isOpen, setIsOpen] = useState(false);
    function onOpenChange({
      open
    }: DrawerOpenChangeDetail) {
      setIsOpen(open);
    }
    function openDrawer() {
      setIsOpen(true);
    }
    return <>
        <Button onClick={openDrawer}>
          Trigger Drawer
        </Button>

        <Drawer onOpenChange={onOpenChange} open={isOpen}>
          <DrawerContent>
            <DrawerBody>
              Content
            </DrawerBody>
          </DrawerContent>
        </Drawer>
      </>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Button, Drawer, DrawerBody, DrawerContent, DrawerTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Trigger Drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent>
        <DrawerBody>
          My drawer content
        </DrawerBody>
      </DrawerContent>
    </Drawer>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Drawer closeOnEscape={arg.closeOnEscape} closeOnInteractOutside={arg.closeOnInteractOutside}>
      <DrawerTrigger asChild>
        <Button>
          Trigger Drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent position={arg.position}>
        <DrawerBody>
          {arg.content}
        </DrawerBody>
      </DrawerContent>
    </Drawer>,
  argTypes: orderControls({
    closeOnEscape: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    closeOnInteractOutside: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    content: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    position: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'DRAWER_POSITION'
        }
      },
      control: {
        type: 'select'
      },
      options: DRAWER_POSITIONS
    }
  }),
  args: {
    content: 'My drawer content'
  }
}
```

### Overlay Elements

```tsx
{
  globals: {
    imports: `import { Button, Drawer, DrawerContent, DrawerBody, DrawerTrigger, ICON_NAME, Icon, Select, SelectContent, SelectControl, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Trigger Drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent>
        <DrawerBody style={{
        display: 'grid',
        columnGap: '8px',
        alignItems: 'center',
        gridTemplateColumns: '1fr max-content'
      }}>
          <Select items={[{
          label: 'Dog',
          value: 'dog'
        }, {
          label: 'Cat',
          value: 'cat'
        }, {
          label: 'Hamster',
          value: 'hamster'
        }, {
          label: 'Parrot',
          value: 'parrot'
        }, {
          label: 'Spider',
          value: 'spider'
        }, {
          label: 'Goldfish',
          value: 'goldfish'
        }]}>
            <SelectControl />

            <SelectContent createPortal={false} />
          </Select>

          <Tooltip>
            <TooltipTrigger asChild>
              <Icon name={ICON_NAME.circleQuestion} style={{
              fontSize: '24px'
            }} />
            </TooltipTrigger>

            <TooltipContent createPortal={false}>
              This is the tooltip content
            </TooltipContent>
          </Tooltip>
        </DrawerBody>
      </DrawerContent>
    </Drawer>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Trigger Drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent position={DRAWER_POSITION.left}>
        <DrawerBody>
          My drawer content
        </DrawerBody>
      </DrawerContent>
    </Drawer>
}
```

### Position

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    alignItems: 'center',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { DRAWER_POSITION, Button, Drawer, DrawerBody, DrawerContent, DrawerTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
    <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Top drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent position={DRAWER_POSITION.top}>
        <DrawerBody>
          Top drawer content
        </DrawerBody>
      </DrawerContent>
    </Drawer>

   <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Left drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent position={DRAWER_POSITION.left}>
        <DrawerBody>
          Left drawer content
        </DrawerBody>
      </DrawerContent>
    </Drawer>

   <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Right Drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent position={DRAWER_POSITION.right}>
        <DrawerBody>
          Right drawer content
        </DrawerBody>
      </DrawerContent>
    </Drawer>

   <Drawer>
      <DrawerTrigger asChild>
        <Button>
          Bottom Drawer
        </Button>
      </DrawerTrigger>

      <DrawerContent position={DRAWER_POSITION.bottom}>
        <DrawerBody>
          Bottom drawer content
        </DrawerBody>
      </DrawerContent>
    </Drawer>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'row wrap',
    gap: '12px'
  }}>
      <Drawer>
        <DrawerTrigger asChild>
          <Button>Left</Button>
        </DrawerTrigger>
        <DrawerContent createPortal={false} position={DRAWER_POSITION.left}>
          <DrawerBody>Left drawer</DrawerBody>
        </DrawerContent>
      </Drawer>

      <Drawer>
        <DrawerTrigger asChild>
          <Button>Right</Button>
        </DrawerTrigger>
        <DrawerContent createPortal={false} position={DRAWER_POSITION.right}>
          <DrawerBody>Right drawer</DrawerBody>
        </DrawerContent>
      </Drawer>

      <Drawer>
        <DrawerTrigger asChild>
          <Button>Top</Button>
        </DrawerTrigger>
        <DrawerContent createPortal={false} position={DRAWER_POSITION.top}>
          <DrawerBody>Top drawer</DrawerBody>
        </DrawerContent>
      </Drawer>

      <Drawer>
        <DrawerTrigger asChild>
          <Button>Bottom</Button>
        </DrawerTrigger>
        <DrawerContent createPortal={false} position={DRAWER_POSITION.bottom}>
          <DrawerBody>Bottom drawer</DrawerBody>
        </DrawerContent>
      </Drawer>
    </div>
}
```

## React Components

# Editable

Double click to edit

## Overview

---

The **Editable** component allows users to edit text directly in place. It replaces static text with an editable field, letting users quickly update content without opening a form or modal.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Editable</td></tr><tr><th scope="row">Also known as</th><td>Inline Editor</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/nrXo7riue1w773W2h4HGBS/ODS---UI-Kit?m=auto&amp;node-id=14291-46529&amp;t=rsFWlOhZe0TlF6Wd-1" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/editable" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Editable** component is used when users need to modify text directly in the interface, without disrupting their workflow.

They are commonly used for:

-   Inline editing of cell values in a datagrid or table (e.g., a nickname)
-   Editable titles or headings (e.g., rename a dashboard)
-   Settings panels (e.g. configuration values, such as thresholds or labels)
-   Text snippets (e.g., section titles, captions)
-   Lists and todo items

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Editable when users need to edit values quickly and frequently. |
| - Define a reasonable max length to prevent layout overflow. |
| - Ensure editable content has a meaningful default or empty state. |

| ❌ Don't |
| --- |
| - Remove visual cues that indicate editability. |
| - Use Editable for long or complex form content. |
| - Use Editable when changes must be reviewed or explicitly confirmed in a separate step. |

### Best Practices in Context

1.  **Editable**
2.  **Preview / Editing field**
3.  **Actions**

Preview displays static text when not in edit mode while the field is visible only during edit mode.

## Behavior

---

### Edit Activation

Users can enter edit mode through different triggers:

-   By click on the edit trigger
-   By double-click on the preview

### Editing

When entering edit mode, the preview is replaced by a field containing the current value.

Users can type in the field and update the current value.

### Submission

The editable value should be committed when the user activates the submit trigger.

### Cancellation

Activating the cancel trigger should discard the current changes and returns the component to its previous value.

### Empty

If the current value is empty, an empty state should be displayed to indicate that no content has been defined yet.

## Navigation

---

### Focus Management

When edit mode is entered, focus should moves to the expected field.

Exiting edit mode returns focus according to the surrounding context.

### General Keyboard Shortcuts

Pressing Tab moves focus forward to the next interactive element.

Pressing Shift + Tab moves focus backward to the previous interactive element.

Pressing Enter on the edit trigger activates edit mode.

Pressing Enter on the submit trigger should commit the current value and exits edit mode.

Pressing Enter on the cancel trigger should discard the current changes and exits edit mode.

## Accessibility

---

Accessibility depends on the form elements used within **Editable**. Make sure inputs, triggers and custom actions follow standard accessibility rules.

## React Components

# Editable

## Overview

---

## Anatomy

---

Editable

EditableActions

EditableCancelTrigger

EditableDisplay

EditableDisplayEmpty

EditableEditTrigger

EditableInput

EditableSubmitTrigger

---

Empty value

---

## Editable

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultEditing

 | `boolean` | - | `undefined` | The initial editing state of the editable. Use when you don't need to control the editing state of the editable. |
| 

editing

 | `boolean` | - | `undefined` | The controlled editing state of the editable. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

onCancel

 | `() => void` | - | `undefined` | Callback fired when the cancel trigger is activated. |
| 

onEditChange

 | `(detail: EditableEditingChangeDetail) => void` | - | `undefined` | Callback fired when the editing state changes. |
| 

onSubmit

 | `() => void` | - | `undefined` | Callback fired when the submit trigger is activated. |

## EditableActions

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
children

 | `(props: { editing: boolean }) => ReactNode` | - | `undefined` | Custom actions render function, that provides Editable current state. |

## EditableCancelTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## EditableDisplay

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## EditableDisplayEmpty

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |

## EditableEditTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## EditableInput

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## EditableSubmitTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## Enums

---

### EDITABLE_I18N

-   cancelButton =`"editable.actions.cancel.button"`
-   editButton =`"editable.actions.edit.button"`
-   submitButton =`"editable.actions.submit.button"`

## Interfaces

---

### EditableEditingChangeDetail

-   `editing: boolean`

## Examples

---

### Default

```jsx
const [value, setValue] = useState('Double click to edit');
  const bufferValue = useRef(value);
  return <Editable onCancel={() => bufferValue.current = value} onSubmit={() => setValue(bufferValue.current)}>
      <EditableDisplay>
        {value || <EditableDisplayEmpty>Empty value</EditableDisplayEmpty>}
      </EditableDisplay>
      <EditableInput>
        <Input autoFocus defaultValue={value} onChange={e => bufferValue.current = e.target.value} />
      </EditableInput>
      <EditableActions />
    </Editable>;
}
```

### Controlled

Double click to edit

```jsx
const [displayValue, setDisplayValue] = useState('Double click to edit');
  const [value, setValue] = useState(displayValue);
  return <Editable onCancel={() => setValue(displayValue)} onSubmit={() => setDisplayValue(value)}>
      <EditableDisplay>
        {displayValue || <EditableDisplayEmpty>Empty value</EditableDisplayEmpty>}
      </EditableDisplay>
      <EditableInput>
        <Input autoFocus onChange={e => setValue(e.target.value)} value={value} />
      </EditableInput>
      <EditableActions />
    </Editable>;
}
```

### Custom Actions

```jsx
const [value, setValue] = useState('Double click to edit');
  const bufferValue = useRef(value);
  return <Editable onCancel={() => bufferValue.current = value} onSubmit={() => setValue(bufferValue.current)}>
      <EditableDisplay>
        {value || <EditableDisplayEmpty>Empty value</EditableDisplayEmpty>}
      </EditableDisplay>
      <EditableInput>
        <Input autoFocus defaultValue={value} onChange={e => bufferValue.current = e.target.value} />
      </EditableInput>
      <EditableActions>
        {({
        editing      }) => editing ? <>
                <EditableSubmitTrigger asChild>
                  <Button size={BUTTON_SIZE.sm}>Submit</Button>
                </EditableSubmitTrigger>
                <EditableCancelTrigger asChild>
                  <Button size={BUTTON_SIZE.sm} variant={BUTTON_VARIANT.outline}>Cancel</Button>
                </EditableCancelTrigger>
              </> : <EditableEditTrigger asChild>
                <Button size={BUTTON_SIZE.sm}>Edit</Button>
              </EditableEditTrigger>}
      </EditableActions>
    </Editable>;
}
```

### Complex Form Element

```jsx
const [displayValue, setDisplayValue] = useState('');
  const [accountValue, setAccountValue] = useState('');
  const [domainValue, setDomainValue] = useState('');
  function onCancel(): void {
    const [account, domain] = displayValue.split('@ovhcloud');
    setAccountValue(account || '');
    setDomainValue(domain || '');
  }
  return <Editable onCancel={onCancel} onSubmit={() => setDisplayValue(`${accountValue}@ovhcloud${domainValue}`)}>
      <EditableDisplay>
        {displayValue || <EditableDisplayEmpty>Account name</EditableDisplayEmpty>}
      </EditableDisplay>
      <EditableInput>
        <FormField style={{
        display: 'flex',
        flexFlow: 'row',
        alignItems: 'center',
        columnGap: '8px'
      }}>
          <Input name="account-name" onChange={e => setAccountValue(e.target.value)} pattern="^([a-zA-Z0-9]|([._\-](?![._\-])))*[a-zA-Z0-9]$" placeholder="Account name" value={accountValue} />
          <span>
            @ovhcloud          </span>
          <Select items={[{
          label: '.fr',
          value: '.fr'
        }, {
          label: '.com',
          value: '.com'
        }, {
          label: '.dev',
          value: '.dev'
        }]} name="domain" onValueChange={({
          value        }) => setDomainValue(value[0])} value={domainValue ? [domainValue] : []}>
            <SelectControl placeholder="Select domain" />
            <SelectContent />
          </Select>
        </FormField>
      </EditableInput>
      <EditableActions />
    </Editable>;
}
```

## Recipes

---

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

## React Components/Editable

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultEditing` | `` | No |  | The initial editing state of the editable. Use when you don't need to control the editing state of the editable. |
| `editing` | `` | No |  | The controlled editing state of the editable. |
| `i18n` | `` | No |  | Internal translations override. |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `onCancel` | `` | No |  | Callback fired when the cancel trigger is activated. |
| `onEditChange` | `` | No |  | Callback fired when the editing state changes. |
| `onSubmit` | `` | No |  | Callback fired when the submit trigger is activated. |


## Subcomponents


### EditableActions




### EditableCancelTrigger

@param {boolean|optional} asChild - Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment.


### EditableDisplay




### EditableDisplayEmpty




### EditableEditTrigger

@param {boolean|optional} asChild - Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment.


### EditableInput




### EditableSubmitTrigger

@param {boolean|optional} asChild - Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment.

## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <div>
      <Editable editing={false}>
        <EditableDisplay>
          <EditableDisplayEmpty>Empty value</EditableDisplayEmpty>
        </EditableDisplay>

        <EditableActions />
      </Editable>

      <Divider spacing={DIVIDER_SPACING._16} />

      <Editable editing={true}>
        <EditableInput>
          <Input />
        </EditableInput>

        <EditableActions />
      </Editable>
    </div>
}
```

### Complex Form Element

```tsx
{
  globals: {
    imports: `import { Editable, EditableActions, EditableDisplay, EditableDisplayEmpty, EditableInput, FormField, Input, Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const [displayValue, setDisplayValue] = useState('');
    const [accountValue, setAccountValue] = useState('');
    const [domainValue, setDomainValue] = useState('');
    function onCancel(): void {
      const [account, domain] = displayValue.split('@ovhcloud');
      setAccountValue(account || '');
      setDomainValue(domain || '');
    }
    return <Editable onCancel={onCancel} onSubmit={() => setDisplayValue(`${accountValue}@ovhcloud${domainValue}`)}>
        <EditableDisplay>
          {displayValue || <EditableDisplayEmpty>Account name</EditableDisplayEmpty>}
        </EditableDisplay>

        <EditableInput>
          <FormField style={{
          display: 'flex',
          flexFlow: 'row',
          alignItems: 'center',
          columnGap: '8px'
        }}>
            <Input name="account-name" onChange={e => setAccountValue(e.target.value)} pattern="^([a-zA-Z0-9]|([._\-](?![._\-])))*[a-zA-Z0-9]$" placeholder="Account name" value={accountValue} />

            <span>
              @ovhcloud
            </span>

            <Select items={[{
            label: '.fr',
            value: '.fr'
          }, {
            label: '.com',
            value: '.com'
          }, {
            label: '.dev',
            value: '.dev'
          }]} name="domain" onValueChange={({
            value
          }) => setDomainValue(value[0])} value={domainValue ? [domainValue] : []}>
              <SelectControl placeholder="Select domain" />

              <SelectContent />
            </Select>
          </FormField>
        </EditableInput>

        <EditableActions />
      </Editable>;
  }
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { Editable, EditableActions, EditableDisplay, EditableDisplayEmpty, EditableInput, Input } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const [displayValue, setDisplayValue] = useState('Double click to edit');
    const [value, setValue] = useState(displayValue);
    return <Editable onCancel={() => setValue(displayValue)} onSubmit={() => setDisplayValue(value)}>
        <EditableDisplay>
          {displayValue || <EditableDisplayEmpty>Empty value</EditableDisplayEmpty>}
        </EditableDisplay>

        <EditableInput>
          <Input autoFocus onChange={e => setValue(e.target.value)} value={value} />
        </EditableInput>

        <EditableActions />
      </Editable>;
  }
}
```

### Custom Actions

```tsx
{
  globals: {
    imports: `import { BUTTON_SIZE, BUTTON_VARIANT, Button, Editable, EditableActions, EditableCancelTrigger, EditableDisplay, EditableDisplayEmpty, EditableEditTrigger, EditableInput, EditableSubmitTrigger, Input } from '@ovhcloud/ods-react';
import { useRef, useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const [value, setValue] = useState('Double click to edit');
    const bufferValue = useRef(value);
    return <Editable onCancel={() => bufferValue.current = value} onSubmit={() => setValue(bufferValue.current)}>
        <EditableDisplay>
          {value || <EditableDisplayEmpty>Empty value</EditableDisplayEmpty>}
        </EditableDisplay>

        <EditableInput>
          <Input autoFocus defaultValue={value} onChange={e => bufferValue.current = e.target.value} />
        </EditableInput>

        <EditableActions>
          {({
          editing
        }) => editing ? <>
                  <EditableSubmitTrigger asChild>
                    <Button size={BUTTON_SIZE.sm}>Submit</Button>
                  </EditableSubmitTrigger>

                  <EditableCancelTrigger asChild>
                    <Button size={BUTTON_SIZE.sm} variant={BUTTON_VARIANT.outline}>Cancel</Button>
                  </EditableCancelTrigger>
                </> : <EditableEditTrigger asChild>
                  <Button size={BUTTON_SIZE.sm}>Edit</Button>
                </EditableEditTrigger>}
        </EditableActions>
      </Editable>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Editable, EditableActions, EditableDisplay, EditableDisplayEmpty, EditableInput, Input } from '@ovhcloud/ods-react';
import { useRef, useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const [value, setValue] = useState('Double click to edit');
    const bufferValue = useRef(value);
    return <Editable onCancel={() => bufferValue.current = value} onSubmit={() => setValue(bufferValue.current)}>
        <EditableDisplay>
          {value || <EditableDisplayEmpty>Empty value</EditableDisplayEmpty>}
        </EditableDisplay>

        <EditableInput>
          <Input autoFocus defaultValue={value} onChange={e => bufferValue.current = e.target.value} />
        </EditableInput>

        <EditableActions />
      </Editable>;
  }
}
```

### Demo

```tsx
{
  render: () => {
    const [value, setValue] = useState('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.');
    const bufferValue = useRef(value);
    return <Editable onCancel={() => bufferValue.current = value} onSubmit={() => setValue(bufferValue.current)} style={{
      display: 'flex'
    }}>
        <EditableDisplay>
          <p style={{
          margin: 0
        }}>
            {value || <EditableDisplayEmpty>Empty value</EditableDisplayEmpty>}
          </p>
        </EditableDisplay>

        <EditableInput style={{
        flex: '1 1 0'
      }}>
          <Textarea autoFocus defaultValue={value} onChange={e => bufferValue.current = e.target.value} style={{
          resize: 'vertical',
          width: '100%'
        }} />
        </EditableInput>

        <EditableActions />
      </Editable>;
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => {
    const [value, setValue] = useState('Double click to edit');
    const bufferValue = useRef(value);
    return <Editable onCancel={() => bufferValue.current = value} onSubmit={() => setValue(bufferValue.current)}>
        <EditableDisplay>
          {value || <EditableDisplayEmpty>Empty value</EditableDisplayEmpty>}
        </EditableDisplay>

        <EditableInput>
          <Input autoFocus defaultValue={value} onChange={e => bufferValue.current = e.target.value} />
        </EditableInput>

        <EditableActions />
      </Editable>;
  }
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <Editable>
      <EditableDisplay>
        Some node
      </EditableDisplay>

      <EditableInput>
        <Input autoFocus defaultValue="Some node" />
      </EditableInput>

      <EditableActions />
    </Editable>
}
```

## React Components

# File Thumbnail

foo.txt3 byte

## Overview

---

File Thumbnail component is a visual representation of an uploaded file, displaying a preview or a standalone icon alongside file metadata. It is designed to be used in file upload interfaces, attachment lists, or media libraries.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>FileThumbnail</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/mwZtfuQ9nzv6fY0dfez4NZ/ODS---UI-Kit?node-id=18865-2253&amp;t=5Qpq63EmcZsiF0hy-0" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/file-thumbnail" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

File Thumbnail component is used to represent uploaded files in a compact format. It provides visual feedback about the file type, name, size, and upload time, while allowing users to remove the file. Common usages include:

-   File upload zones: display attached files after selection or upload.
-   Message composers: show file attachments before sending.
-   Media libraries: list uploaded documents, images, or assets.
-   Form fields: represent file inputs with selected file previews.

### Best Practices in Context

1.  **FileThumbnail**

TODO

## Behavior

---

### Content & Display

Thumbnail / Preview:

-   Displays an image preview for supported image types.
-   Falls back to a generic file icon for non-image files.
-   The thumbnail image is resized and centered to avoid distortion.

Fallback priority:

-   Image preview (if supported format and loads successfully).
-   Generic file icon (if type is unknown or unrecognized).

File Metadata:

-   File name and extension, displayed with ellipsis truncation if too long; full name exposed via title attribute.
-   File size, displayed in human-readable format (e.g., 340 KB, 1.2 MB, 3.4 GB).

Remove Action:

-   A remove button always visible.
-   Clicking remove should either:
    -   Immediately removes the component with a brief undo toast.
    -   Triggers an inline confirmation step before removal.

### States

-   Default: Thumbnail or fallback icon displayed with full metadata and remove button.
-   Read-only: Removal button disabled.
-   Loading: Progress Bar shown in place of file size while file uploads.
-   Error: Error icon shown with a short error message (e.g., "Preview unavailable" or "Upload failed"); file name and extension remain visible.

## Navigation

---

The component is non-interactive by default, except for the remove button.

The remove button receives keyboard focus via Tab.

Pressing Enter or Space on the focused remove button triggers the remove action.

Focus moves to the next thumbnail or the previous interactive element after removal.

General Keyboard Shortcuts:

-   Tab : Moves focus to the remove button of the thumbnail.
-   Enter / Space : Activates the focused remove button.
-   Shift + Tab : Moves focus backward to the previous interactive element.

## Accessibility

---

Each **File Thumbnail** must be wrapped in a `role="listitem"` element, and its parent container must have `role="list"` to allow screen readers to navigate between thumbnails.

On remove, if a confirmation step is triggered, focus must move to the confirmation dialog or inline prompt.

## React Components

# File Thumbnail

## Overview

---

## Anatomy

---

FileThumbnail

---

## FileThumbnail

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

dismissible

 | `boolean` | - | `true` | Whether the component holds a dismiss button. |
| 

error

 | `string` | - | `undefined` | The file error message to display. |
| 

file

 | `File` |  | `undefined` | The current File object. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for size formatting and the translation of the internal elements. |
| 

onFileRemove

 | `(detail: FileThumbnailRemoveDetail) => void` | - | `undefined` | Callback fired when the file is removed. |
| 

progress

 | `number` | - | `undefined` | The file upload progress. |

## Enums

---

### FILE_THUMBNAIL_I18N

-   cancelButton =`"fileThumbnail.file.cancel.button"`
-   deleteButton =`"fileThumbnail.file.delete.button"`
-   progressBar =`"fileThumbnail.file.progressBar"`

## Interfaces

---

### FileThumbnailRemoveDetail

-   `file: File`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-file-thumbnail-border-color | var(--ods-color-neutral-300) | 
 |
| --ods-file-thumbnail-object-fit | contain | 

 |
| --ods-file-thumbnail-preview-size | 32px | 

 |

## Examples

---

### Default

foo.txt3 byte

```jsx
const fakeFile = new File(['foo'], 'foo.txt', {
    type: 'text/plain'
  });
  return <FileThumbnail file={fakeFile} />;
}
```

### Progress

foo.txt

​

```jsx
const fakeFile = new File(['foo'], 'foo.txt', {
    type: 'text/plain'
  });
  return <FileThumbnail file={fakeFile} progress={45} />;
}
```

### Disabled

foo.txt3 byte

```jsx
const fakeFile = new File(['foo'], 'foo.txt', {
    type: 'text/plain'
  });
  return <FileThumbnail disabled file={fakeFile} />;
}
```

### Non-dismissible

non-dismissible-file.txt3 byte

```jsx
const fakeFile = new File(['foo'], 'non-dismissible-file.txt', {
    type: 'text/plain'
  });
  return <FileThumbnail dismissible={false} file={fakeFile} />;
}
```

### Error

```jsx
const fakeFile = new File(['foo'], 'foo.txt', {
    type: 'text/plain'
  });
  return <FileThumbnail error="Something went wrong" file={fakeFile} />;
}
```

## Recipes

---

Chat

Assistant2:58 PM

Welcome to the Chat recipe. Feel free to test the UI behavior by typing anything on your mind below.

## React Components/File Thumbnail

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `disabled` | `` | No |  | Whether the component is disabled. |
| `dismissible` | `` | No | true | Whether the component holds a dismiss button. |
| `error` | `` | No |  | The file error message to display. |
| `file` | `` | Yes |  | The current File object. |
| `i18n` | `` | No |  | Internal translations override. |
| `locale` | `` | No |  | The locale used for size formatting and the translation of the internal elements. |
| `onFileRemove` | `` | No |  | Callback fired when the file is removed. |
| `progress` | `` | No |  | The file upload progress. |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => {
    const fakeFile = new File(['foo'], 'foo.txt', {
      type: 'text/plain'
    });
    return <FileThumbnail file={fakeFile} />;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { FileThumbnail } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const fakeFile = new File(['foo'], 'foo.txt', {
      type: 'text/plain'
    });
    return <FileThumbnail file={fakeFile} />;
  }
}
```

### Demo

```tsx
{
  render: ({
    disabled,
    dismissible,
    error,
    progress
  }) => {
    const [file, setFile] = useState<File>();
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      rowGap: '16px'
    }}>
        <input onChange={e => {
        if (e.target.files?.length) {
          setFile(e.target.files[0]);
        }
      }} type="file" />
        {file && <FileThumbnail disabled={disabled} dismissible={dismissible} error={error} file={file} progress={progress} />}
      </div>;
  },
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    dismissible: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    error: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    progress: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'number'
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { FileThumbnail } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const fakeFile = new File(['foo'], 'foo.txt', {
      type: 'text/plain'
    });
    return <FileThumbnail disabled file={fakeFile} />;
  }
}
```

### Error

```tsx
{
  globals: {
    imports: `import { FileThumbnail } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const fakeFile = new File(['foo'], 'foo.txt', {
      type: 'text/plain'
    });
    return <FileThumbnail error="Something went wrong" file={fakeFile} />;
  }
}
```

### Non Dismissible

```tsx
{
  globals: {
    imports: `import { FileThumbnail } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const fakeFile = new File(['foo'], 'non-dismissible-file.txt', {
      type: 'text/plain'
    });
    return <FileThumbnail dismissible={false} file={fakeFile} />;
  }
}
```

### Overview

```tsx
{
  parameters: {
    layout: 'centered'
  },
  tags: ['!dev'],
  render: ({}) => {
    const fakeFile = new File(['foo'], 'foo.txt', {
      type: 'text/plain'
    });
    return <FileThumbnail file={fakeFile} />;
  }
}
```

### Progress

```tsx
{
  globals: {
    imports: `import { FileThumbnail } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const fakeFile = new File(['foo'], 'foo.txt', {
      type: 'text/plain'
    });
    return <FileThumbnail file={fakeFile} progress={45} />;
  }
}
```

### Theme Generator

```tsx
{
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    },
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => {
    const fakeFile = new File(['foo'], 'foo.txt', {
      type: 'text/plain'
    });
    return <FileThumbnail file={fakeFile} />;
  }
}
```

## React Components

# File Upload

_**File Upload** allows users to select one or more files to upload to a specific location._

Drag & drop a file 

## Overview

---

The **File Upload** component is used to enable users to upload files to a server or application.

It provides a way to browse, select, and upload files, and is accompanied by progress indicators and feedback messages to guide users through the process.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>File Upload</td></tr><tr><th scope="row">Also known as</th><td>File input, File uploader, Dropzone</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=55-24358" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/file-upload" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-file-upload--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

Use the **File Upload** component when you need users to submit files, such as documents, images, or other data.

Common use cases include form submissions, profile picture updates, and document management systems.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a single File Upload component per context when allowing users to upload multiple files on the same page |
| - Clearly communicate constraints such as file type, size limits, or max number of files |
| - Use short and clear success message once the upload is complete |
| - Ensure error messages are specific and actionable (e.g., “File exceeds 5MB limit” instead of a generic “Upload failed”) |

| ❌ Don't |
| --- |
| - Don't place the File Upload component inside a modal unless absolutely necessary |
| - Don't allow file uploads without providing feedback or confirmation |
| - Don't assume users know upload limitations. Make restrictions explicit |

### Best Practices in Context

1.  **File Upload**
2.  **Description** - optional (file limitations)
3.  **Dropzone**
4.  **Upload button**
5.  **Uploaded files zone**
6.  **Delete button**

## Placement

---

**File Upload** component can be part of a complex form, or displayed as a standalone.

**File Upload** has an automatic default width, based on its content.

## Behavior

---

### Selecting files

When selecting the "Browse" Button, the native file selection popup opens to retrieve the needed file to upload.

### Drag & Drop

Users can drag & drop a file, the drop zone being the whole **File Upload** component.

### Uploading files

When a file upload is in progress, the file name and its size (if available) are displayed.

A Spinner is displayed to indicate the file is uploading. It remains visible as long as the file uploading is not finished yet. The uploading progress percentage can be indicated next to the file name.

A close icon Button is displayed as soon as a file upload is in progress. It is displayed as long as the user does not interrupt the upload or removes an already uploaded file.

### Upload complete

#### With success

The Spinner is replaced with the file icon, the file name and the icon turn green, as soon as a file has been uploaded, meaning the upload is successful and complete.

#### With error

If the uploading of a file went wrong, the Spinner is replaced with an error icon, and the file name and the icon turn red. A specific `error` message may be displayed below the uploaded file.

The user is still able to upload another file but the file in error will remain in error until user removes it.

Depending on the error, the global component can be displayed in an error state.

### Extra behavior

#### Global configuration / error

Number of files, maximum sizes and expected formats can be set globally inside the component.

A global `error` message is displayed when:

-   Wrong file format has been uploaded
-   Maximum file size exceeded
-   Maximum number of files exceeded

The global component is displayed in an error state in this situation.

#### Disabled

When **File Upload** is disabled, user cannot interact with the Button and drag & drop feature is not available.

#### Cancel a file upload in progress

A click or press on the `close` icon Button interrupts the upload (or cancel an uploaded file).

When the upload is canceled, the file upload is removed and the **File Upload** component returns to its default state, except for any other files in the queue.

In case some other files have already been uploaded, and the user cancels an uploading file, the next one will take its place in the queue and so on.

#### Remove an already uploaded file

An already uploaded file can be removed by clicking or pressing the `close` icon Button anytime.

Same as cancelling a file while it is uploading, if several files have already been uploaded and the user cancels an uploading file, the next one will take its place in the queue and so on.

## Navigation

---

### Focus Management

When tabbing through the page, the **File Upload** trigger button receives focus as part of the natural tab order.

If one or more files are being uploaded or already uploaded, each close icon button becomes focusable in the tab order, in the order files were added.

If the component is disabled, it cannot receive focus and no interaction is possible.

The drag-and-drop area does not receive focus but accepts dropped files while the component is enabled.

### General Keyboard Shortcuts

Pressing Tab moves focus forward through the **File Upload** trigger button and all available close icon buttons.

Pressing Shift + Tab moves focus backward through these same elements.

Pressing Enter or Space while the trigger button is focused opens the native file selection dialog.

Pressing Enter or Space on a close icon button cancels an upload in progress or removes the corresponding uploaded file from the list.

## Accessibility

---

To ensure proper accessibility, the **File Upload** component must be correctly labeled and provide meaningful context when interactive elements (such as icon buttons) are used.

### Always provide an explicit label

Every **File Upload** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Files:

Drag & drop a file 

```jsx
const [files, setFiles] = useState<File[]>([]);
  return <FormField>
      <FormFieldLabel>
        Files:      </FormFieldLabel>
      <FileUpload onFileAccept={({
      files    }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
        </FileUploadList>
      </FileUpload>
    </FormField>;
}
```

Screen readers will announce the label, the field and its content.

### Labeling cancel/delete Button

The `Cancel` and `Delete` file buttons have to be explicit by adding context about their action and the file name.

Drag & drop a file 

```jsx
const [files, setFiles] = useState<File[]>([]);
  return <FileUpload onFileAccept={({
    files  }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
      <FileUploadList>
        {files.map((file: File, idx) => <FileUploadItem file={file} i18n={{
        [FILE_UPLOAD_I18N.cancelButton]: `Cancel uploading ${file.name}`,
        [FILE_UPLOAD_I18N.deleteButton]: `Remove ${file.name}`,
        [FILE_UPLOAD_I18N.progressBar]: `Uploading ${file.name}`
      }} key={idx} />)}
      </FileUploadList>
    </FileUpload>;
}
```

Screen readers will announce the action and the file name.

## React Components

# File Upload

## Overview

---

## Anatomy

---

FileUpload

FileUploadItem

FileUploadList

---

## FileUpload

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
accept

 | `string` | - | `undefined` | The accepted file types. |
| 

acceptedFileLabel

 | `string` | - | `undefined` | Label describing the accepted file types. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

dropzoneLabel

 | `string` | - | `'Drag & drop a file'` | The dropzone label. |
| 

error

 | `string` | - | `undefined` | The global error message to display. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

maxFile

 | `number` | - | `Infinity` | The maximum number of files that can be selected. |
| 

maxFileLabel

 | `string` | - | `undefined` | Label describing the maximum number of files that can be selected. |
| 

maxSize

 | `number` | - | `undefined` | The maximum size of selectable files. |
| 

maxSizeLabel

 | `string` | - | `undefined` | Label describing the maximum size of selectable files. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onFileAccept

 | `(detail: FileUploadAcceptDetail) => void` | - | `undefined` | Callback fired when a some files have been successfully added. |
| 

onFileReject

 | `(detail: FileUploadRejectDetail) => void` | - | `undefined` | Callback fired when a some files have been rejected. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. |
| 

triggerLabel

 | `string` | - | `'Browse files'` | Upload button label. |
| 

variant

 | `FILE_UPLOAD_VARIANT` | - | `FILE_UPLOAD_VARIANT.default` | The variant preset to use. |

## FileUploadItem

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [li attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/li#attributes) . |
| 
error

 | `string` | - | `undefined` | The file error message to display. |
| 

file

 | `File` |  | `undefined` | The current File object. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

progress

 | `number` | - | `undefined` | The file upload progress. |
| 

uploadSuccessLabelDeprecated

 | `string` | - | `undefined` | The label displayed after a successful upload. DEPRECATED: Latest design change removed the upload success label in favor of a visual icon update. |

## FileUploadList

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [ul attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ul#attributes) . |

## Enums

---

### FILE_REJECTION_CAUSE

-   invalidFile =`"invalid-file"`
-   maxFileReached =`"max-file-reached"`
-   sizeTooLarge =`"size-too-large"`
-   unknownError =`"unknown-error"`
-   wrongFormat =`"wrong-format"`

### FILE_UPLOAD_I18N

-   cancelButton =`"fileUpload.file.cancel.button"`
-   deleteButton =`"fileUpload.file.delete.button"`
-   progressBar =`"fileUpload.file.progressBar"`

### FILE_UPLOAD_VARIANT

-   compact =`"compact"`
-   default =`"default"`

## Interfaces

---

### FileUploadAcceptDetail

-   `files: File[]`

### FileUploadRejectDetail

-   `files: { errors: FILE_REJECTION_CAUSE[], file: File }[]`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-file-upload-item-background-color-hover | var(--ods-color-primary-050) | 
 |
| --ods-file-upload-item-border-radius | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-file-upload-padding-horizontal | var(--ods-theme-padding-horizontal) | 

 |
| --ods-file-upload-padding-vertical | var(--ods-theme-padding-vertical) | 

 |

## Examples

---

### Default

Drag & drop a file 

```jsx
const [files, setFiles] = useState<File[]>([]);
  return <FileUpload onFileAccept={({
    files  }) => setFiles(files)}>
      <FileUploadList>
        {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
      </FileUploadList>
    </FileUpload>;
}
```

### Compact

Drag & drop a file 

```jsx
const [files, setFiles] = useState<File[]>([]);
  return <FileUpload onFileAccept={({
    files  }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
      <FileUploadList>
        {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
      </FileUploadList>
    </FileUpload>;
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { FileUpload, FileUploadList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FileUpload disabled variant={FILE_UPLOAD_VARIANT.compact}>
      <FileUploadList />
    </FileUpload>
}
```

### Custom labels

Nombre maximal de fichiers : 3Taille de fichier max : 524 MBFormats acceptés : images

Glisser-déposer des fichiers 

```jsx
const [files, setFiles] = useState<File[]>([]);
  return <FileUpload acceptedFileLabel="Formats acceptés : images" dropzoneLabel="Glisser-déposer des fichiers" maxFile={3} maxFileLabel="Nombre maximal de fichiers :" maxSize={524288000} maxSizeLabel="Taille de fichier max :" onFileAccept={({
    files  }) => setFiles(files)} triggerLabel="Parcourir les fichiers" variant={FILE_UPLOAD_VARIANT.compact}>
      <FileUploadList>
        {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} progress={100} uploadSuccessLabel="Fichier uploadé" />)}
      </FileUploadList>
    </FileUpload>;
}
```

### Accept only specific file type

To limit the file types that can be uploaded, you can use the `accept` attribute.

It works exactly the same as the one from the native [input file](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) .

```jsx
const [error, setError] = useState<string>('');
  const [files, setFiles] = useState<File[]>([]);  function onAccept({    files  }: FileUploadAcceptDetail): void {
    setFiles(files);
    setError('');
  }
  function onReject({    files  }: FileUploadRejectDetail): void {
    setError(files.length ? 'File(s) not of the expected format' : '');
  }
  return <FileUpload accept="image/png" acceptedFileLabel="Png files only" error={error} onFileAccept={onAccept} onFileReject={onReject} variant={FILE_UPLOAD_VARIANT.compact}>
      <FileUploadList>
        {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
      </FileUploadList>
    </FileUpload>;
}
```

### Limit the maximum number of file

```jsx
const [error, setError] = useState<string>('');
  const [files, setFiles] = useState<File[]>([]);  function onAccept({    files  }: FileUploadAcceptDetail): void {
    setFiles(files);
    setError('');
  }
  function onReject({    files  }: FileUploadRejectDetail): void {
    setError(files.length ? 'Too many files' : '');
  }
  return <FileUpload error={error} maxFile={3} maxFileLabel="Maximum file allowed:" onFileAccept={onAccept} onFileReject={onReject} variant={FILE_UPLOAD_VARIANT.compact}>
      <FileUploadList>
        {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
      </FileUploadList>
    </FileUpload>;
}
```

### Limit the maximum size of each file

```jsx
const [error, setError] = useState<string>('');
  const [files, setFiles] = useState<File[]>([]);  function onAccept({    files  }: FileUploadAcceptDetail): void {
    setFiles(files);
    setError('');
  }
  function onReject({    files  }: FileUploadRejectDetail): void {
    setError(files.length ? 'File(s) too large' : '');
  }
  return <FileUpload error={error} maxSize={1000000} maxSizeLabel="No file larger than:" onFileAccept={onAccept} onFileReject={onReject} variant={FILE_UPLOAD_VARIANT.compact}>
      <FileUploadList>
        {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
      </FileUploadList>
    </FileUpload>;
}
```

### Display upload progress

```jsx
type MyFile = File & {
    error?: string;
    progress?: number;
  };
  const [files, setFiles] = useState<MyFile[]>([]);
  useEffect(() => {
    files.forEach(file => {
      if (!file.progress) {
        uploadFile(file);
      }
    });
  }, [files]);
  function uploadFile(file: MyFile): void {
    const intervalId = setInterval(() => {
      setFiles(files => files.map(f => {
        if (f.name === file.name) {
          f.progress = (f.progress || 0) + Math.floor(Math.random() * 10 + 1);
          if (f.progress >= 100) {
            clearInterval(intervalId);
          }
        }
        return f;
      }));
    }, 100);
  }
  return <FileUpload onFileAccept={({
    files  }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
      <FileUploadList>
        {files.map((file, idx) => <FileUploadItem error={file.error} file={file} key={idx} progress={file.progress} />)}
      </FileUploadList>
    </FileUpload>;
}
```

### Form Field

```jsx
const [files, setFiles] = useState<File[]>([]);
  return <FormField>
        <FormFieldLabel>
          Files:        </FormFieldLabel>
        <FileUpload onFileAccept={({
      files    }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
          <FileUploadList>
            {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
          </FileUploadList>
        </FileUpload>
      </FormField>;
}
```

## Recipes

---

No recipe defined for now.

## React Components/File Upload

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `accept` | `` | No |  | The accepted file types. |
| `acceptedFileLabel` | `` | No |  | Label describing the accepted file types. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `dropzoneLabel` | `` | No | 'Drag & drop a file' | The dropzone label. |
| `error` | `` | No |  | The global error message to display. |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `maxFile` | `` | No | Infinity | The maximum number of files that can be selected. |
| `maxFileLabel` | `` | No |  | Label describing the maximum number of files that can be selected. |
| `maxSize` | `` | No |  | The maximum size of selectable files. |
| `maxSizeLabel` | `` | No |  | Label describing the maximum size of selectable files. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onFileAccept` | `` | No |  | Callback fired when a some files have been successfully added. |
| `onFileReject` | `` | No |  | Callback fired when a some files have been rejected. |
| `required` | `` | No |  | Whether the component is required. |
| `triggerLabel` | `` | No | 'Browse files' | Upload button label. |
| `variant` | `` | No | FILE_UPLOAD_VARIANT.default | The variant preset to use. |


## Subcomponents


### FileUploadItem



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `error` | `` | No |  | The file error message to display. |
| `file` | `` | Yes |  | The current File object. |
| `i18n` | `` | No |  | Internal translations override. |
| `progress` | `` | No |  | The file upload progress. |
| `uploadSuccessLabel` | `` | No |  | @deprecated The label displayed after a successful upload. DEPRECATED: Latest design change removed the upload success label in favor of a visual icon update. |



### FileUploadList



## Examples


### Accept

```tsx
{
  globals: {
    imports: `import { FileUpload, FileUploadItem, FileUploadList } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [error, setError] = useState<string>('');
    const [files, setFiles] = useState<File[]>([]);
    function onAccept({
      files
    }: FileUploadAcceptDetail): void {
      setFiles(files);
      setError('');
    }
    function onReject({
      files
    }: FileUploadRejectDetail): void {
      setError(files.length ? 'File(s) not of the expected format' : '');
    }
    return <FileUpload accept="image/png" acceptedFileLabel="Png files only" error={error} onFileAccept={onAccept} onFileReject={onReject} variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

### Accessibility File Button

```tsx
{
  globals: {
    imports: `import { FILE_UPLOAD_I18N, FileUpload, FileUploadItem, FileUploadList } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [files, setFiles] = useState<File[]>([]);
    return <FileUpload onFileAccept={({
      files
    }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} i18n={{
          [FILE_UPLOAD_I18N.cancelButton]: `Cancel uploading ${file.name}`,
          [FILE_UPLOAD_I18N.deleteButton]: `Remove ${file.name}`,
          [FILE_UPLOAD_I18N.progressBar]: `Uploading ${file.name}`
        }} key={idx} />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { FileUpload, FileUploadItem, FileUploadList, FormField, FormFieldLabel } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [files, setFiles] = useState<File[]>([]);
    return <FormField>
        <FormFieldLabel>
          Files:
        </FormFieldLabel>

        <FileUpload onFileAccept={({
        files
      }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
          <FileUploadList>
            {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
          </FileUploadList>
        </FileUpload>
      </FormField>;
  }
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => {
    const [files, setFiles] = useState<File[]>([new File(['foo'], 'foo.txt', {
      type: 'text/plain'
    }), new File(['dummy'], 'dummy.txt', {
      type: 'text/plain'
    })]);
    return <FileUpload onFileAccept={({
      files
    }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

### Compact

```tsx
{
  globals: {
    imports: `import { FILE_UPLOAD_VARIANT, FileUpload, FileUploadItem, FileUploadList } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [files, setFiles] = useState<File[]>([]);
    return <FileUpload onFileAccept={({
      files
    }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

### Custom Labels

```tsx
{
  globals: {
    imports: `import { FileUpload, FileUploadItem, FileUploadList } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [files, setFiles] = useState<File[]>([]);
    return <FileUpload acceptedFileLabel="Formats acceptés : images" dropzoneLabel="Glisser-déposer des fichiers" maxFile={3} maxFileLabel="Nombre maximal de fichiers :" maxSize={524288000} maxSizeLabel="Taille de fichier max :" onFileAccept={({
      files
    }) => setFiles(files)} triggerLabel="Parcourir les fichiers" variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} progress={100} uploadSuccessLabel="Fichier uploadé" />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { FileUpload, FileUploadItem, FileUploadList } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [files, setFiles] = useState<File[]>([]);
    return <FileUpload onFileAccept={({
      files
    }) => setFiles(files)}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

### Demo

```tsx
{
  render: arg => {
    const [error, setError] = useState<string>('');
    const [files, setFiles] = useState<File[]>([]);
    function onAccept({
      files
    }: FileUploadAcceptDetail): void {
      setFiles(files);
      setError('');
    }
    function onReject({
      files
    }: FileUploadRejectDetail): void {
      setError(files.length ? 'File(s) rejected' : '');
    }
    return <FileUpload {...arg} error={arg.error || error} onFileAccept={onAccept} onFileReject={onReject}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
        </FileUploadList>
      </FileUpload>;
  },
  argTypes: orderControls({
    acceptedFileLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    dropzoneLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    error: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    maxFile: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'number'
    },
    maxFileLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    maxSize: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'number'
    },
    maxSizeLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    triggerLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    variant: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'FILE_UPLOAD_VARIANT'
        }
      },
      control: {
        type: 'select'
      },
      options: FILE_UPLOAD_VARIANTS
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { FileUpload, FileUploadList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FileUpload disabled variant={FILE_UPLOAD_VARIANT.compact}>
      <FileUploadList />
    </FileUpload>
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { FileUpload, FileUploadItem, FileUploadList, FormField, FormFieldLabel } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [files, setFiles] = useState<File[]>([]);
    return <FormField>
          <FormFieldLabel>
            Files:
          </FormFieldLabel>

          <FileUpload onFileAccept={({
        files
      }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
            <FileUploadList>
              {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
            </FileUploadList>
          </FileUpload>
        </FormField>;
  }
}
```

### Max File

```tsx
{
  globals: {
    imports: `import { FileUpload, FileUploadItem, FileUploadList } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [error, setError] = useState<string>('');
    const [files, setFiles] = useState<File[]>([]);
    function onAccept({
      files
    }: FileUploadAcceptDetail): void {
      setFiles(files);
      setError('');
    }
    function onReject({
      files
    }: FileUploadRejectDetail): void {
      setError(files.length ? 'Too many files' : '');
    }
    return <FileUpload error={error} maxFile={3} maxFileLabel="Maximum file allowed:" onFileAccept={onAccept} onFileReject={onReject} variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

### Max Size

```tsx
{
  globals: {
    imports: `import { FileUpload, FileUploadItem, FileUploadList } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [error, setError] = useState<string>('');
    const [files, setFiles] = useState<File[]>([]);
    function onAccept({
      files
    }: FileUploadAcceptDetail): void {
      setFiles(files);
      setError('');
    }
    function onReject({
      files
    }: FileUploadRejectDetail): void {
      setError(files.length ? 'File(s) too large' : '');
    }
    return <FileUpload error={error} maxSize={1000000} maxSizeLabel="No file larger than:" onFileAccept={onAccept} onFileReject={onReject} variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => {
    const [files, setFiles] = useState<File[]>([]);
    return <FileUpload onFileAccept={({
      files
    }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => {
    const [files, setFiles] = useState<File[]>([]);
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      gap: '12px'
    }}>
        <FileUpload onFileAccept={({
        files
      }) => setFiles(files)}>
          <FileUploadList>
            {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
          </FileUploadList>
        </FileUpload>

        <FileUpload disabled>
          <FileUploadList />
        </FileUpload>

        <FileUpload invalid onFileAccept={({
        files
      }) => setFiles(files)}>
          <FileUploadList>
            {files.map((file: File, idx) => <FileUploadItem file={file} key={idx} />)}
          </FileUploadList>
        </FileUpload>
      </div>;
  }
}
```

### Upload

```tsx
{
  globals: {
    imports: `import { FileUpload, FileUploadItem, FileUploadList } from '@ovhcloud/ods-react';
import { useEffect, useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    type MyFile = File & {
      error?: string;
      progress?: number;
    };
    const [files, setFiles] = useState<MyFile[]>([]);
    useEffect(() => {
      files.forEach(file => {
        if (!file.progress) {
          uploadFile(file);
        }
      });
    }, [files]);
    function uploadFile(file: MyFile): void {
      const intervalId = setInterval(() => {
        setFiles(files => files.map(f => {
          if (f.name === file.name) {
            f.progress = (f.progress || 0) + Math.floor(Math.random() * 10 + 1);
            if (f.progress >= 100) {
              clearInterval(intervalId);
            }
          }
          return f;
        }));
      }, 100);
    }
    return <FileUpload onFileAccept={({
      files
    }) => setFiles(files)} variant={FILE_UPLOAD_VARIANT.compact}>
        <FileUploadList>
          {files.map((file, idx) => <FileUploadItem error={file.error} file={file} key={idx} progress={file.progress} />)}
        </FileUploadList>
      </FileUpload>;
  }
}
```

## React Components

# Form Field

_A **Form Field** component is used to wrap several form components with logic, visual hints and additional styling_

Description:Helper text0/200

## Overview

---

A **Form Field** is based on a single form element or group components as a whole that are decorated with additional text to handle specific types of information, like a Password field for example.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Form Field</td></tr><tr><th scope="row">Also known as</th><td>Form Control</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=172-11996" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/form-field" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-form-field--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

A **Form Field** is used to wrap a field that can be customized with some additional information about this field.

Additional information (label, placeholder, helper message) provides hint and help to users, so that they can easily understand what is required from them in a form.

Users will also type or enter information in the expected format and avoid mistakes.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Form Field to wrap form inputs with a clear structure, including a label, optional helper text, or extra information |
| - Group related fields using multiple Form Fields to improve clarity and scannability |
| - Include helper messages to guide users on expected input formats or constraints (e.g., password requirements) |
| - Use error message to provide feedback after user interaction |

| ❌ Don't |
| --- |
| - Don't place interactive elements like links or buttons inside the label |
| - Don't rely on an asterisk alone to indicate required fields. Prefer explicit helper text or label |
| - Overuse content related Form Fields with top aligned label in one group since it requires quite vertical space: split into smaller groups |
| - Don't use Form Field when there's no input or interaction expected. It is meant for form inputs, not static content |

### Best Practices in Context

1.  **Form Field**
2.  **Label** - optional
3.  **Form element**
4.  **Helper message** - optional
5.  **Error message** - optional
6.  **Hint text** - optional

## Placement

---

A **Form Field** can be used everywhere in a page where there is a form and users may need help to fill in this form.

## Behavior

---

A **Form Field** can react to validation states, such as error, often by visually updating elements like the border or text indicators.

Helper text and error messages can be displayed freely around the field component, depending on the design or functional needs.

## Navigation

---

The **Form Field** component itself is not focusable and does not receive keyboard focus.

Only the interactive inner component it wraps, such as an Input, Password, Select, or other form element, can receive focus. Focus behavior and keyboard navigation are determined by the specific form element used within the **Form Field**.

## Accessibility

---

The **Form Field** component handles by itself the accessibility requirements by using its internal components.

Login:

Username or email address

```jsx
<FormField>
  <FormFieldLabel>
    Login:  </FormFieldLabel>
  <Input name="input" />
  <FormFieldHelper>
    Username or email address  </FormFieldHelper>
</FormField>
```

Screen readers will announce the label, the field, its content and the helper text.

## React Components

# Form Field

## Overview

---

To learn more about **Form Field** usage, please refer to the .

## Anatomy

---

FormField

FormFieldError

FormFieldHelper

FormFieldLabel

FormFieldLabelSubLabel

---

## FormField

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
id

 | `string` | - | `undefined` | The field id. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |

## FormFieldError

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |

## FormFieldHelper

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |

## FormFieldLabel

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [label attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/label#attributes) . |

## FormFieldLabelSubLabel

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-form-field-label-column-gap | calc(var(--ods-theme-column-gap) / 2) | 
 |
| --ods-form-field-label-row-gap | calc(var(--ods-theme-row-gap) / 2) | 

 |
| --ods-form-field-row-gap | calc(var(--ods-theme-row-gap) / 2) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { FormField, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <Textarea name="textarea" />
    </FormField>
}
```

### Field label

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Description:      </FormFieldLabel>
      <Textarea name="textarea" />
    </FormField>
}
```

### Field sub label

Description- mandatory

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, FormFieldLabelSubLabel, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Description        <FormFieldLabelSubLabel>- mandatory</FormFieldLabelSubLabel>
      </FormFieldLabel>
      <Textarea name="textarea" />
    </FormField>
}
```

### Helper message

Helper text

```jsx
{
  globals: {
    imports: `import { TEXT_PRESET, FormField, FormFieldHelper, Text, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <Textarea name="textarea" />
      <FormFieldHelper>
        <Text preset={TEXT_PRESET.caption}>
          Helper text        </Text>
      </FormFieldHelper>
    </FormField>
}
```

### Error message

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldError, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField invalid>
      <Textarea name="textarea" />
      <FormFieldError>
        Error message      </FormFieldError>
    </FormField>
}
```

## Recipes

---

Email Field

Email- mandatory

@

.fr.com.dev

The part before the email address (the text before the @) must follow these guidelines:

-   It must end with a letter or a number
-   Allowed special characters are: ".", "_", "-"
-   Special characters cannot be placed next to each other

## React Components/Form Field

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `id` | `` | No |  | The field id. |
| `invalid` | `` | No |  | Whether the component is in error state. |


## Subcomponents


### FormFieldError




### FormFieldHelper




### FormFieldLabel




### FormFieldLabelSubLabel



## Examples


### Accessibility Label

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldHelper, FormFieldLabel, Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Login:
      </FormFieldLabel>

      <Input name="input" />

      <FormFieldHelper>
        Username or email address
      </FormFieldHelper>
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <FormField invalid>
      <FormFieldLabel>
        Description:
        <FormFieldLabelSubLabel>
          - mandatory
        </FormFieldLabelSubLabel>
      </FormFieldLabel>

      <Textarea name="description" />

      <FormFieldHelper>
        <Text preset={TEXT_PRESET.caption}>
          Helper text
        </Text>
      </FormFieldHelper>

      <FormFieldError>
        Error message
      </FormFieldError>
    </FormField>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { FormField, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <Textarea name="textarea" />
    </FormField>
}
```

### Demo

```tsx
{
  render: (args: DemoArg) => <FormField invalid={args.invalid}>
      <FormFieldLabel>
        {args.label}
        <FormFieldLabelSubLabel>
          {args.sublabel}
        </FormFieldLabelSubLabel>
      </FormFieldLabel>

      <Textarea name="demo" />

      <FormFieldHelper>
        {args.helperText}
      </FormFieldHelper>

      <FormFieldError>
        {args.errorText}
      </FormFieldError>
    </FormField>,
  argTypes: orderControls({
    errorText: {
      table: {
        category: CONTROL_CATEGORY.slot,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    helperText: {
      table: {
        category: CONTROL_CATEGORY.slot,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    label: {
      table: {
        category: CONTROL_CATEGORY.slot,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    sublabel: {
      table: {
        category: CONTROL_CATEGORY.slot,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    }
  })
}
```

### Error

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldError, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField invalid>
      <Textarea name="textarea" />

      <FormFieldError>
        Error message
      </FormFieldError>
    </FormField>
}
```

### Guide Form Critical

```tsx
{
  globals: {
    imports: `import { BUTTON_COLOR, BUTTON_VARIANT, Button, FormField, FormFieldHelper, FormFieldLabel, FormFieldLabelSubLabel, Input, TEXT_PRESET, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => {
    const [localError, setLocalError] = useState<Record<string, string | undefined>>({});
    function onBlur(e: FormEvent): void {
      const target = e.target as HTMLInputElement;
      setLocalError(error => ({
        ...error,
        [target.name]: target.validity.valid ? undefined : target.validationMessage
      }));
    }
    return <form style={{
      display: 'flex',
      flexFlow: 'column',
      rowGap: '8px'
    }}>
        <FormField>
          <FormFieldLabel>
            Please type DELETE to confirm
            <FormFieldLabelSubLabel>
              - mandatory
            </FormFieldLabelSubLabel>
          </FormFieldLabel>

          <Input invalid={!!localError.confirmation} name="confirmation" onBlur={onBlur} pattern="^DELETE$" placeholder="DELETE" required />

          <FormFieldHelper>
            <Text preset={TEXT_PRESET.caption}>
              This action is irreversible.
            </Text>
          </FormFieldHelper>
        </FormField>

        <div style={{
        display: "flex",
        justifyContent: "flex-end",
        gap: "16px"
      }}>
          <Button variant={BUTTON_VARIANT.outline}>
            Cancel
          </Button>

          <Button color={BUTTON_COLOR.critical}>
            Delete account
          </Button>
        </div>
      </form>;
  }
}
```

### Guide Form Error

```tsx
{
  globals: {
    imports: `import { BUTTON_COLOR, BUTTON_VARIANT, Button, FormField, FormFieldError, FormFieldHelper, FormFieldLabel, FormFieldLabelSubLabel, Input, Message, MessageBody, TEXT_PRESET, Text } from '@ovhcloud/ods-react';
import { type FormEvent, useState } from 'react';`
  },
  tags: ['!dev'],
  render: ({}) => {
    const [localError, setLocalError] = useState<Record<string, string | undefined>>({});
    const [globalError, setGlobalError] = useState('');
    const [isSubmitting, setIsSubmitting] = useState(false);
    function handleSubmit(e: FormEvent): void {
      e.preventDefault();
      setGlobalError('');
      setIsSubmitting(true);

      // Fake API call
      setTimeout(() => {
        setGlobalError('Something went wrong');
        setIsSubmitting(false);
      }, 1000);
    }
    function onBlur(e: FormEvent): void {
      const target = e.target as HTMLInputElement;
      setLocalError(error => ({
        ...error,
        [target.name]: target.validity.valid ? undefined : target.validationMessage
      }));
    }
    function onInvalid(e: FormEvent): void {
      e.preventDefault();
      const target = e.target as HTMLInputElement;
      setLocalError(error => ({
        ...error,
        [target.name]: target.validationMessage
      }));
    }
    return <form onSubmit={handleSubmit} style={{
      display: 'flex',
      flexFlow: 'column',
      rowGap: '8px'
    }}>
        <FormField invalid={!!localError.firstname}>
          <FormFieldLabel>
            First name
            <FormFieldLabelSubLabel>
              - mandatory
            </FormFieldLabelSubLabel>
          </FormFieldLabel>

          <Input invalid={!!localError.firstname} name="firstname" onBlur={onBlur} onInvalid={onInvalid} placeholder="Type your first name" required />

          <FormFieldError>
            {localError.firstname}
          </FormFieldError>
        </FormField>

        <FormField invalid={!!localError.email}>
          <FormFieldLabel>
            Email
            <FormFieldLabelSubLabel>
              - mandatory
            </FormFieldLabelSubLabel>
          </FormFieldLabel>

          <Input invalid={!!localError.email} name="email" onBlur={onBlur} onInvalid={onInvalid} pattern="^((?!\.)[\w\-_.]*[^.])(@\w+)(\.\w+(\.\w+)?[^.\W])$" placeholder="Type your email" required />

          <FormFieldError>
            {localError.email}
          </FormFieldError>
        </FormField>

        {!!globalError && <Message color={MESSAGE_COLOR.critical} onRemove={() => setGlobalError('')}>
            <MessageBody>
              {globalError}
            </MessageBody>
          </Message>}

        <div style={{
        display: "flex",
        justifyContent: "flex-end",
        gap: "16px"
      }}>
          <Button disabled={isSubmitting} variant={BUTTON_VARIANT.outline}>
            Cancel
          </Button>

          <Button color={BUTTON_COLOR.critical} loading={isSubmitting} type="submit">
            Delete account
          </Button>
        </div>
      </form>;
  }
}
```

### Guide Form Grouped Field

```tsx
{
  globals: {
    imports: `import { Button, FormField, FormFieldLabel, FormFieldLabelSubLabel, Input, TEXT_PRESET, Text } from '@ovhcloud/ods-react';
import { type FormEvent, useState } from 'react';`
  },
  tags: ['!dev'],
  render: ({}) => {
    const [localError, setLocalError] = useState<Record<string, string | undefined>>({});
    function onBlur(e: FormEvent): void {
      const target = e.target as HTMLInputElement;
      setLocalError(error => ({
        ...error,
        [target.name]: target.validity.valid ? undefined : target.validationMessage
      }));
    }
    return <form style={{
      display: 'flex',
      flexFlow: 'column',
      rowGap: '8px'
    }}>
        <Text preset={TEXT_PRESET.heading3}>
          Personal Information
        </Text>

        <FormField>
          <FormFieldLabel>
            First name
            <FormFieldLabelSubLabel>
              - mandatory
            </FormFieldLabelSubLabel>
          </FormFieldLabel>

          <Input invalid={!!localError.firstname} name="firstname" onBlur={onBlur} placeholder="Type your first name" required />
        </FormField>

        <FormField>
          <FormFieldLabel>
            Last name
            <FormFieldLabelSubLabel>
              - mandatory
            </FormFieldLabelSubLabel>
          </FormFieldLabel>

          <Input invalid={!!localError.lastname} name="lastname" onBlur={onBlur} placeholder="Type your last name" required />
        </FormField>

        <Text preset={TEXT_PRESET.heading3}>
          Company Information
        </Text>

        <FormField>
          <FormFieldLabel>
            Company name
          </FormFieldLabel>

          <Input name="company" placeholder="OVHcloud" />
        </FormField>

        <FormField>
          <FormFieldLabel>
            VAT number
          </FormFieldLabel>

          <Input name="vat" placeholder="vat" />
        </FormField>

        <div style={{
        display: "flex",
        justifyContent: "center"
      }}>
          <Button>
            Save
          </Button>
        </div>
      </form>;
  }
}
```

### Guide Form Mandatory

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldHelper, FormFieldLabel, FormFieldLabelSubLabel, Input, PhoneNumber, PhoneNumberControl, TEXT_PRESET, Text, Textarea } from '@ovhcloud/ods-react';
import { type FormEvent, useState } from 'react';`
  },
  tags: ['!dev'],
  render: ({}) => {
    const [localError, setLocalError] = useState<Record<string, string | undefined>>({});
    function onBlur(e: FormEvent): void {
      const target = e.target as HTMLInputElement;
      setLocalError(error => ({
        ...error,
        [target.name]: target.validity.valid ? undefined : target.validationMessage
      }));
    }
    return <form style={{
      display: 'flex',
      flexFlow: 'column',
      rowGap: '8px'
    }}>
        <FormField>
          <FormFieldLabel>
            First name
            <FormFieldLabelSubLabel>
              - mandatory
            </FormFieldLabelSubLabel>
          </FormFieldLabel>

          <Input invalid={!!localError.firstname} name="firstname" onBlur={onBlur} placeholder="Type your first name" required />
        </FormField>

        <FormField>
          <FormFieldLabel>
            Last name
            <FormFieldLabelSubLabel>
              - mandatory
            </FormFieldLabelSubLabel>
          </FormFieldLabel>

          <Input invalid={!!localError.lastname} name="lastname" onBlur={onBlur} placeholder="Type your last name" required />
        </FormField>

        <FormField>
          <FormFieldLabel>
            Company
          </FormFieldLabel>

          <Input name="company" placeholder="Company" />
        </FormField>

        <FormField>
          <FormFieldLabel>
            Phone
          </FormFieldLabel>

          <PhoneNumber name="phonenumber">
            <PhoneNumberControl />
          </PhoneNumber>

          <FormFieldHelper>
            <Text preset={TEXT_PRESET.caption}>
              Include country code (e.g. +33 6 00 00 00 00)
            </Text>
          </FormFieldHelper>
        </FormField>

        <FormField>
          <FormFieldLabel>
            Email
            <FormFieldLabelSubLabel>
              - mandatory
            </FormFieldLabelSubLabel>
          </FormFieldLabel>

          <Input invalid={!!localError.email} name="email" onBlur={onBlur} pattern="^((?!\.)[\w\-_.]*[^.])(@\w+)(\.\w+(\.\w+)?[^.\W])$" placeholder="Your email" required type="email" />

          <FormFieldHelper>
            <Text preset={TEXT_PRESET.caption}>
              Format: name@example.com
            </Text>
          </FormFieldHelper>
        </FormField>
      </form>;
  }
}
```

### Guide Form Simple

```tsx
{
  globals: {
    imports: `import { BUTTON_VARIANT, Button, FormField, FormFieldLabel, PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <form style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>
      <FormField>
        <FormFieldLabel>
          Phone number
        </FormFieldLabel>

        <PhoneNumber name="phonenumber">
          <PhoneNumberCountryList />
          <PhoneNumberControl />
        </PhoneNumber>
      </FormField>

      <div style={{
      display: "flex",
      justifyContent: "flex-end",
      gap: "16px"
    }}>
        <Button variant={BUTTON_VARIANT.outline}>
          Back
        </Button>

        <Button>
          Update profile
        </Button>
      </div>
    </form>
}
```

### Helper

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, FormField, FormFieldHelper, Text, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <Textarea name="textarea" />

      <FormFieldHelper>
        <Text preset={TEXT_PRESET.caption}>
          Helper text
        </Text>
      </FormFieldHelper>
    </FormField>
}
```

### Label

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Description:
      </FormFieldLabel>

      <Textarea name="textarea" />
    </FormField>
}
```

### Label Sub Label

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, FormFieldLabelSubLabel, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Description
        <FormFieldLabelSubLabel>- mandatory</FormFieldLabelSubLabel>
      </FormFieldLabel>

      <Textarea name="textarea" />
    </FormField>
}
```

### Overview

```tsx
{
  parameters: {
    layout: 'centered'
  },
  tags: ['!dev'],
  render: ({}) => {
    const MAX_COUNT = 200;
    const [count, setCount] = useState(0);
    function onInput(e: FormEvent): void {
      setCount((e.target as HTMLTextAreaElement).value.length);
    }
    return <FormField invalid={count > MAX_COUNT}>
        <FormFieldLabel>
          Description:
        </FormFieldLabel>

        <Textarea name="description" onInput={onInput} />

        <FormFieldHelper style={{
        display: 'flex',
        justifyContent: 'space-between'
      }}>
          <Text preset={TEXT_PRESET.caption}>
            Helper text
          </Text>

          <Text preset={TEXT_PRESET.caption}>
            {count}/{MAX_COUNT}
          </Text>
        </FormFieldHelper>

        <FormFieldError>
          Error message
        </FormFieldError>
      </FormField>;
  }
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px'
  }}>
      <FormField>
        <FormFieldLabel>Label</FormFieldLabel>
        <Input name="input" />
      </FormField>

      <FormField>
        <Input name="input" />
        <FormFieldHelper>Helper text</FormFieldHelper>
      </FormField>

      <FormField invalid>
        <Input name="input" />
        <FormFieldError>Error message</FormFieldError>
      </FormField>
    </div>
}
```

## React Components

# Icon

_**Icon** is a visual context used to represent a command, navigation, status or common action._

## Overview

---

**Icon** is a visual symbol which provides visual context thanks to its clarity and consistency.

It is displayed as solid, in a single color.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Icon</td></tr><tr><th scope="row">Also known as</th><td>Symbol</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=26-20213" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/icon" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-icon--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Icons** enhance the usability and bring clarity to users by reducing the cognitive load.

**Icons** can be used in standalone Button and can be usually associated with some sort of action.

**Icons** can be used as navigational elements as well.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Ensure sufficient contrast between the Icon color and its background for accessibility compliance |
| - Center-align Icon vertically when placed next to text to maintain visual balance |
| - Use the same color for the Icon and the adjacent text when they are paired for a unified appearance |
| - Use Icon to support meaning, such as reinforcing an action or clarifying status |
| - Ensure consistent sizing and spacing across the interface for visual harmony |

| ❌ Don't |
| --- |
| - Don't use the same Icon for different meanings. It can confuse users and reduce semantic clarity |
| - Don't use similar shades for both the Icon and background, this can reduce visibility |
| - Don't baseline-align Icon when next to text, it disrupts alignment and flow |
| - Don't use different colors for the Icon and the adjacent text when they represent the same concept |
| - Don't rely on Icon without supporting text for critical actions or information |

## Placement

---

**Icons** draw attention to important information or action, so it has to be thoughtfully inserted in a page.

They can also help with the understanding of specific elements by providing a visual example.

They also can be used in other components where it is adapted for a specific purpose.

## Behavior

---

**Icons** can stand alone or be included in a button or other components depending on the usage.

## Navigation

---

The **Icon** component is non-interactive and does not receive keyboard focus when used alone. It is purely decorative unless included in an interactive component (e.g., a button, a link, or a breadcrumb).

When used within an interactive component, refer to that component's documentation for keyboard navigation details.

## Accessibility

---

Proper attributes should be used to ensure that **Icons** are either perceivable by assistive technologies or correctly hidden when decorative.

### Hiding decorative Icon

By default, every **Icon** has a `role="presentation"` that hides it from screen readers.

### Providing meaningful information

If the **Icon** conveys essential information, it must be accompanied by an accessible label using [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) and [role="img"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/img_role) .

```jsx
<Icon
  aria-label="home"
  name="home"
  role="img"
/>
```

Screen readers will announce the label, ensuring users understand the **Icon**'s meaning.

If the **Icon** is inside an interactive element, prefer using [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) on the element itself rather than on the **Icon**. See  examples.

## React Components

# Icon Gallery

-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-   
-

## React Components

# Icon

## Overview

---

## Anatomy

---

Icon

---

## Icon

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |
| 
name

 | `ICON_NAME` |  | `undefined` | The icon name. |

## Enums

---

### ICON_NAME

-   accessibility =`"accessibility"`
-   accessibilityFull =`"accessibility-full"`
-   analysis =`"analysis"`
-   arrowCrossed =`"arrow-crossed"`
-   arrowDown =`"arrow-down"`
-   arrowDownLeft =`"arrow-down-left"`
-   arrowDownRight =`"arrow-down-right"`
-   arrowLeft =`"arrow-left"`
-   arrowLeftRight =`"arrow-left-right"`
-   arrowRight =`"arrow-right"`
-   arrowUp =`"arrow-up"`
-   arrowUpDown =`"arrow-up-down"`
-   arrowUpLeft =`"arrow-up-left"`
-   arrowUpRight =`"arrow-up-right"`
-   bell =`"bell"`
-   bill =`"bill"`
-   blog =`"blog"`
-   book =`"book"`
-   box =`"box"`
-   brain =`"brain"`
-   building =`"building"`
-   cable =`"cable"`
-   calculator =`"calculator"`
-   calendar =`"calendar"`
-   castle =`"castle"`
-   chainLink =`"chain-link"`
-   check =`"check"`
-   chevronDoubleLeft =`"chevron-double-left"`
-   chevronDoubleRight =`"chevron-double-right"`
-   chevronDown =`"chevron-down"`
-   chevronLeft =`"chevron-left"`
-   chevronLeftSlash =`"chevron-left-slash"`
-   chevronLeftUnderscore =`"chevron-left-underscore"`
-   chevronRight =`"chevron-right"`
-   chevronUp =`"chevron-up"`
-   circleCheck =`"circle-check"`
-   circleCheckFull =`"circle-check-full"`
-   circleInfo =`"circle-info"`
-   circleInfoFull =`"circle-info-full"`
-   circleQuestion =`"circle-question"`
-   circleQuestionFull =`"circle-question-full"`
-   circleThreeNodes =`"circle-three-nodes"`
-   circleUser =`"circle-user"`
-   circleXmark =`"circle-xmark"`
-   circleXmarkFull =`"circle-xmark-full"`
-   clockTimeFour =`"clock-time-four"`
-   clockTimeNine =`"clock-time-nine"`
-   clockTimeSix =`"clock-time-six"`
-   clockTimeThree =`"clock-time-three"`
-   clockTimeTwelve =`"clock-time-twelve"`
-   cloud =`"cloud"`
-   cloudCheck =`"cloud-check"`
-   cloudDownload =`"cloud-download"`
-   cloudLock =`"cloud-lock"`
-   cloudUpload =`"cloud-upload"`
-   cloudXmark =`"cloud-xmark"`
-   cog =`"cog"`
-   columns =`"columns"`
-   comment =`"comment"`
-   cpu =`"cpu"`
-   creditCard =`"credit-card"`
-   crown =`"crown"`
-   dPad =`"d-pad"`
-   diagnostic =`"diagnostic"`
-   diamondExclamation =`"diamond-exclamation"`
-   diamondExclamationFull =`"diamond-exclamation-full"`
-   diamondsFull =`"diamonds-full"`
-   discord =`"discord"`
-   disk =`"disk"`
-   download =`"download"`
-   dragDrop =`"drag-drop"`
-   ellipsisHorizontal =`"ellipsis-horizontal"`
-   ellipsisVertical =`"ellipsis-vertical"`
-   email =`"email"`
-   emoticon =`"emoticon"`
-   emoticonDizzy =`"emoticon-dizzy"`
-   emoticonNeutral =`"emoticon-neutral"`
-   emoticonSad =`"emoticon-sad"`
-   emoticonSmile =`"emoticon-smile"`
-   emoticonWink =`"emoticon-wink"`
-   equal =`"equal"`
-   externalLink =`"external-link"`
-   eye =`"eye"`
-   eyeOff =`"eye-off"`
-   facebook =`"facebook"`
-   fiber =`"fiber"`
-   file =`"file"`
-   fileCopy =`"file-copy"`
-   fileMinus =`"file-minus"`
-   filePlus =`"file-plus"`
-   filter =`"filter"`
-   flame =`"flame"`
-   flaskEmpty =`"flask-empty"`
-   flaskFull =`"flask-full"`
-   flaskHalf =`"flask-half"`
-   floppy =`"floppy"`
-   focus =`"focus"`
-   folder =`"folder"`
-   folderMinus =`"folder-minus"`
-   folderPlus =`"folder-plus"`
-   funnel =`"funnel"`
-   gameConsole =`"game-console"`
-   gameController =`"game-controller"`
-   gameControllerAlt =`"game-controller-alt"`
-   gathering =`"gathering"`
-   gift =`"gift"`
-   github =`"github"`
-   globe =`"globe"`
-   grid =`"grid"`
-   gridAlt =`"grid-alt"`
-   hamburgerMenu =`"hamburger-menu"`
-   handshake =`"handshake"`
-   headphones =`"headphones"`
-   heart =`"heart"`
-   heartFull =`"heart-full"`
-   heartHalf =`"heart-half"`
-   hexagonExclamation =`"hexagon-exclamation"`
-   hexagonExclamationFull =`"hexagon-exclamation-full"`
-   hierarchy =`"hierarchy"`
-   history =`"history"`
-   home =`"home"`
-   key =`"key"`
-   keyboard =`"keyboard"`
-   leaf =`"leaf"`
-   lifeBuoy =`"lifebuoy"`
-   lightbulb =`"lightbulb"`
-   lightning =`"lightning"`
-   linkedin =`"linkedin"`
-   list =`"list"`
-   location =`"location"`
-   lockClose =`"lock-close"`
-   lockOpen =`"lock-open"`
-   magicWand =`"magic-wand"`
-   magnifyingGlass =`"magnifying-glass"`
-   meter =`"meter"`
-   mic =`"mic"`
-   micOff =`"mic-off"`
-   minus =`"minus"`
-   money =`"money"`
-   moneyBagDefault =`"money-bag-default"`
-   moneyBagDollar =`"money-bag-dollar"`
-   moneyBagEuro =`"money-bag-euro"`
-   monitor =`"monitor"`
-   moon =`"moon"`
-   network =`"network"`
-   paperclip =`"paperclip"`
-   pause =`"pause"`
-   pauseFull =`"pause-full"`
-   pen =`"pen"`
-   percent =`"percent"`
-   phone =`"phone"`
-   piggyBank =`"piggy-bank"`
-   pin =`"pin"`
-   pinOff =`"pin-off"`
-   play =`"play"`
-   playFull =`"play-full"`
-   plus =`"plus"`
-   printer =`"printer"`
-   question =`"question"`
-   reddit =`"reddit"`
-   refresh =`"refresh"`
-   resize =`"resize"`
-   robot =`"robot"`
-   server =`"server"`
-   serverRack =`"server-rack"`
-   shareNodes =`"share-nodes"`
-   shareScreen =`"share-screen"`
-   shield =`"shield"`
-   shieldCheck =`"shield-check"`
-   shieldExclamation =`"shield-exclamation"`
-   shieldFirewall =`"shield-firewall"`
-   shieldLock =`"shield-lock"`
-   shieldMinus =`"shield-minus"`
-   shieldOff =`"shield-off"`
-   shieldPlus =`"shield-plus"`
-   shieldXmark =`"shield-xmark"`
-   shoppingCart =`"shopping-cart"`
-   shoppingCartError =`"shopping-cart-error"`
-   shoppingCartMinus =`"shopping-cart-minus"`
-   shoppingCartPlus =`"shopping-cart-plus"`
-   shoppingCartXmark =`"shopping-cart-xmark"`
-   shrink =`"shrink"`
-   shutdown =`"shutdown"`
-   snowflake =`"snowflake"`
-   sortAlphaDown =`"sort-alpha-down"`
-   sortAlphaUp =`"sort-alpha-up"`
-   sortNumericDown =`"sort-numeric-down"`
-   sortNumericUp =`"sort-numeric-up"`
-   sparkle =`"sparkle"`
-   splitHorizontal =`"split-horizontal"`
-   splitVertical =`"split-vertical"`
-   sslKey =`"ssl-key"`
-   star =`"star"`
-   starFull =`"star-full"`
-   starHalf =`"star-half"`
-   stop =`"stop"`
-   stopFull =`"stop-full"`
-   store =`"store"`
-   suitcase =`"suitcase"`
-   sun =`"sun"`
-   sync =`"sync"`
-   tag =`"tag"`
-   target =`"target"`
-   theme =`"theme"`
-   thumbsDown =`"thumbs-down"`
-   thumbsUp =`"thumbs-up"`
-   timer =`"timer"`
-   trafficCone =`"traffic-cone"`
-   trash =`"trash"`
-   triangleExclamation =`"triangle-exclamation"`
-   triangleExclamationFull =`"triangle-exclamation-full"`
-   triangleThreeNodes =`"triangle-three-nodes"`
-   truck =`"truck"`
-   twitch =`"twitch"`
-   undo =`"undo"`
-   upload =`"upload"`
-   user =`"user"`
-   userFull =`"user-full"`
-   vault =`"vault"`
-   x =`"x"`
-   xmark =`"xmark"`
-   youtube =`"youtube"`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon name={ICON_NAME.home} />
}
```

### Decorative

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon name={ICON_NAME.home} />
}
```

### Informative

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon aria-label="Help" name={ICON_NAME.circleQuestion} role="img" />
}
```

## Recipes

---

Chat

Assistant2:58 PM

Welcome to the Chat recipe. Feel free to test the UI behavior by typing anything on your mind below.

Config Tile

VPS 1

4 vCore8 Go RAM100 Go 1 day automated backup Unlimited traffic 200 Mbps

12 months6 monthsNo commitment

From€24.46ex. VAT/monthor €13.19 incl. VAT/month

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

Feature List

-   Memory: up to 1.5 TB
    
-   SLA: 99.99%
    
-   Guaranteed public bandwidth from 5 Gbps to 25 Gbps
    
-   25 Gbps private bandwidth included
    
-   OVHcloud Link Aggregation
    

Feature List Product Card

WEB HOSTING

NewBest seller

Performance

For demanding online stores and projects.

1 vCore 2,4 GHz, 2 Go RAM1 vCore 2,4 GHz, 4 Go RAM2 vCores 2,4 GHz, 8 Go RAM

From

€24.46ex. VAT/month

or €13.19 incl. VAT/monthfor a 24-month registration

Minimum 2-year registration €100 free with a 5-year registration

Installation fee:Free

-   -   Unlimited websites
        
    -   High power level
        
    -   1 domain name free for the first
        
    -   500 GB SSD storage
        
    -   1,000 email addresses
        
-   1-click CMS
    
    -   WordPress
        
    -   Joomla!
        
    -   Drupal
        
    -   Prestashop
        
-   Database
    
    -   4 x 1 GB databases
        
    -   8 GB Web Cloud Databases
        
-   Security
    
    -   Unlimited free SSL
        
    -   Anti-DDoS protection
        
    -   Anti-virus and anti-spam
        
    -   Daily backups
        
-   Performance
    
    -   99.9% observed availability
        
    -   Guaranteed resources
        
    -   Unlimited traffic
        
    -   Service continuity
        
    -   Boost option to withstand temporary traffic spikes
        
-   Support and additional services
    
    -   Git
        
    -   Standard support
        
    -   SSH access
        
    -   CDN Basic
        

Media Product Card

AI Deploy

Easily deploy machine learning models and applications into production, create your API access points with ease, and make effective predictions.

Order Button

## React Components/Icon

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `name` | `` | Yes |  | The icon name. |


## Examples


### Accessibility Bad Practice Decorative

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon name={ICON_NAME.home} />
}
```

### Accessibility Bad Practice Rating

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <label htmlFor="rating">
        Rating
      </label>

      <div id="rating">
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="one star" role="img" />
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="two star" role="img" />
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="three star" role="img" />
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="four star" role="img" />
        <Icon name={ICON_NAME.star} tabIndex={0} aria-label="five star" role="img" />
      </div>
    </>
}
```

### Accessibility Informative

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon aria-label="home" name={ICON_NAME.home} role="img" />
}
```

### Accessibility Rating

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <label htmlFor="rating" id="rating-label">
        Rating
      </label>

      <div aria-labelledby="rating-label" id="rating" role="radiogroup">
        <Icon name={ICON_NAME.star} role="radio" tabIndex={-1} aria-label="one star" aria-checked="false" />
        <Icon name={ICON_NAME.star} role="radio" tabIndex={0} aria-label="two star" aria-checked="true" />
        <Icon name={ICON_NAME.star} role="radio" tabIndex={-1} aria-label="three star" aria-checked="false" />
        <Icon name={ICON_NAME.star} role="radio" tabIndex={-1} aria-label="four star" aria-checked="false" />
        <Icon name={ICON_NAME.star} role="radio" tabIndex={-1} aria-label="five star" aria-checked="false" />
      </div>
    </>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Icon name="home" style={{
    fontSize: '2rem',
    color: 'var(--ods-color-primary-500)'
  }} />
}
```

### Decorative

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon name={ICON_NAME.home} />
}
```

### Default

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon name={ICON_NAME.home} />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    name: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'ICON_NAME'
        }
      },
      control: {
        type: 'select'
      },
      options: ICON_NAMES
    }
  }),
  args: {
    name: ICON_NAME.home
  }
}
```

### Informative

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Icon aria-label="Help" name={ICON_NAME.circleQuestion} role="img" />
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Icon name="home" style={{
    fontSize: '2rem',
    color: 'var(--ods-color-primary-500)'
  }} />
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'row wrap',
    gap: '16px',
    alignItems: 'center'
  }}>
      <Icon name={ICON_NAME.home} />
      <Icon name={ICON_NAME.circleInfo} />
      <Icon name={ICON_NAME.tag} />
      <Icon name={ICON_NAME.check} />
      <Icon name={ICON_NAME.triangleExclamation} />
    </div>
}
```

## React Components

# Input

_**Input** component is an **Input** field where users can type into_

## Overview

---

An **Input** is used to allow the user to enter a single line information in a field.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Input</td></tr><tr><th scope="row">Also known as</th><td>Text Input, Text Field</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=93-22570" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/input" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-input--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

An **Input** is used to type a free-form short text in a field. It is often used within a Form Field component that adds its label.

It is commonly used in a form, such as asking the user their name or email address, data entered can be text or numbers.

The **Input** component can be used in addition with an **Autocomplete**.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Always pair the Input with a clear and explicit label, using the Form Field component for example |
| - Provide a helper message or validation feedback below the Input when appropriate (e.g., expected format or error state) |
| - Use the clearable button option when it helps users reset the field quickly and efficiently |
| - Use the toggle mask button for sensitive inputs to let users show or hide the content |
| - Include a search button within the Input if a specific action is expected (e.g., triggering a search) |
| - Choose the appropriate input type (email, password, search, etc.) to support native validation |

| ❌ Don't |
| --- |
| - Don't use an Input for multi-line content, use a Textarea instead |
| - Don't use an Input when users must choose from a predefined set of answers. Prefer Select, Combobox, or Radio |
| - Don't leave an Input without a label |
| - Don't overload the Input with multiple embedded actions (e.g., clear + search + toggle) since it creates visual clutter |
| - Don't use an Input that is inappropriately sized for its content |

### Best Practices in Context

1.  **Input**
2.  **Placeholder or input text**
3.  **Clearable button** - optional

## Placement

---

**Input** should be vertically aligned with other form components on a same page.

**Inputs** that are strongly related in a form can be grouped. This group can flow horizontally left to right and/or vertically top to bottom.

## Behavior

---

**Inputs** can be hovered, focused and clicked / triggered. They can be disabled or readonly as well.

The user can start typing in the **Input** after clicking or focusing on the field container.

A clearable button can be displayed within the **Input** to clear its content. This button becomes visible as soon as the user types the first character and removes all content when clicked.

A toggle mask button can be displayed within the **Input** to toggle the visibility of its content, such as revealing or hiding a password in a masked input.

A search button can be displayed within the **Input** to trigger a specific action, such as performing a search.

## Navigation

---

### Focus Management

The **Input** field receives focus as part of the natural tab order.

If a clear button is present, it becomes focusable immediately after the **Input** field.

If a toggle mask button (show/hide password) is present, it becomes focusable after the clear button.

If a search button is present, it becomes focusable after the other **Input** action buttons.

If the **Input** field is read-only, it can still receive focus but cannot be edited.

If the **Input** is disabled, it is skipped in the tab order and cannot be edited.

### General Keyboard Shortcuts

Pressing Tab moves focus forward.

Pressing Shift + Tab moves focus backward to the previous interactive element.

Typing any character while the **Input** field is focused enters text into the field.

Pressing Backspace deletes the character preceding the cursor.

Pressing Enter while a clear, toggle mask, or search button is focused triggers the corresponding action.

## Accessibility

---

To ensure proper accessibility, the **Input** component must be correctly labeled and provide meaningful context when interactive elements (such as icon buttons) are used.

### Always provide an explicit label

Every **Input** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Name:

```jsx
<FormField>
  <FormFieldLabel>
    Name:  </FormFieldLabel>
  <Input />
</FormField>
```

Screen readers will announce the label, the field and its content.

### Override action context

To provide more context on the interactive elements, you can provide your own custom translations to the component.

Search:

```jsx
<FormField>
  <FormFieldLabel>
    Search:  </FormFieldLabel>
  <Input clearable defaultValue="my search" i18n={{
  [INPUT_I18N.clearButton]: 'Clear current search',
  [INPUT_I18N.searchButton]: 'Search in database'
}} type='search' />
</FormField>
```

Screen readers will announce the label, the field, its content and custom label of focused action.

## React Components

# Input

## Overview

---

## Anatomy

---

Input

---

## Input

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [input attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attributes) . |
| 
clearable

 | `boolean` | - | `false` | Whether the clear button is displayed. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

loading

 | `boolean` | - | `false` | Whether the component is in loading state. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

maskOption

 | `object` | - | `{ enable: false, initialState: INPUT_MASK_STATE.close }` | Masked display options. |
| 

enable

 | `boolean` |  | `-` | Whether the masked display is active. |
| 

initialState

 | `INPUT_MASK_STATE` | - | `-` | Initial state of the mask. |
| 

onClear

 | `() => void` | - | `undefined` | Callback fired when the input value is cleared. |
| 

type

 | `INPUT_TYPE` | - | `INPUT_TYPE.text` | The input type. |

## Enums

---

### INPUT_I18N

-   clearButton =`"input.clear.button"`
-   maskButtonHide =`"input.mask.hide.button"`
-   maskButtonShow =`"input.mask.show.button"`
-   searchButton =`"input.search.button"`

### INPUT_MASK_STATE

-   close =`"close"`
-   open =`"open"`

### INPUT_TYPE

-   email =`"email"`
-   number =`"number"`
-   password =`"password"`
-   search =`"search"`
-   text =`"text"`
-   time =`"time"`
-   url =`"url"`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input />
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input disabled />
}
```

### Readonly

```jsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input defaultValue="Readonly" readOnly />
}
```

### Clearable

```jsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input clearable defaultValue="Clearable" />
}
```

### Loading

```jsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input loading />
}
```

### Masked

```jsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input maskOption={{
    enable: true
  }} />
}
```

### Types

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px',
    alignItems: 'start'
  }}>{story()}</div>],
  globals: {
    imports: `import { INPUT_TYPE, Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Input type={INPUT_TYPE.email} placeholder="email" /><br />
      <Input type={INPUT_TYPE.number} placeholder="number" /><br />
      <Input type={INPUT_TYPE.password} placeholder="password" /><br />
      <Input type={INPUT_TYPE.search} placeholder="search" /><br />
      <Input type={INPUT_TYPE.text} placeholder="text" /><br />
      <Input type={INPUT_TYPE.time} placeholder="time" /><br />
      <Input type={INPUT_TYPE.url} placeholder="url" /><br />
    </>
}
```

### Floating number

```jsx
{
  globals: {
    imports: `import { INPUT_TYPE, Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input defaultValue="9.99" step="any" type={INPUT_TYPE.number} />
}
```

### Datalist

```jsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Input list="ice-cream-flavors" />
      <datalist id="ice-cream-flavors">
        <option value="Chocolate"></option>
        <option value="Coconut"></option>
        <option value="Mint"></option>
        <option value="Strawberry"></option>
        <option value="Vanilla"></option>
      </datalist>
    </>
}
```

### Form field

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Name:      </FormFieldLabel>
      <Input />
    </FormField>
}
```

## Recipes

---

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

Email Field

Email- mandatory

@

.fr.com.dev

The part before the email address (the text before the @) must follow these guidelines:

-   It must end with a letter or a number
-   Allowed special characters are: ".", "_", "-"
-   Special characters cannot be placed next to each other

Range Input

0100

## React Components/Input

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `clearable` | `` | No | false | Whether the clear button is displayed. |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `i18n` | `` | No |  | Internal translations override. |
| `loading` | `` | No | false | Whether the component is in loading state. |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `maskOption` | `` | No | { enable: false, initialState: INPUT_MASK_STATE.close } | Masked display options. |
| `onClear` | `` | No |  | Callback fired when the input value is cleared. |
| `type` | `` | No | INPUT_TYPE.text | The input type. |


## Examples


### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Name:
      </FormFieldLabel>

      <Input />
    </FormField>
}
```

### Accessibility I 18 N

```tsx
{
  globals: {
    imports: `import { INPUT_I18N, FormField, FormFieldLabel, Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => <FormField>
      <FormFieldLabel>
        Search:
      </FormFieldLabel>

      <Input clearable defaultValue="my search" i18n={{
      [INPUT_I18N.clearButton]: 'Clear current search',
      [INPUT_I18N.searchButton]: 'Search in database'
    }} type='search' />
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Input placeholder="Input" />
}
```

### Clearable

```tsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input clearable defaultValue="Clearable" />
}
```

### Datalist

```tsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Input list="ice-cream-flavors" />

      <datalist id="ice-cream-flavors">
        <option value="Chocolate"></option>
        <option value="Coconut"></option>
        <option value="Mint"></option>
        <option value="Strawberry"></option>
        <option value="Vanilla"></option>
      </datalist>
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input />
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => {
    const {
      masked,
      ...inputArg
    } = arg;
    return <Input maskOption={{
      enable: !!masked
    }} {...inputArg} />;
  },
  argTypes: orderControls({
    clearable: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    loading: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    masked: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    placeholder: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    type: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'INPUT_TYPE'
        }
      },
      control: {
        type: 'select'
      },
      options: INPUT_TYPES
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input disabled />
}
```

### Floating Number

```tsx
{
  globals: {
    imports: `import { INPUT_TYPE, Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input defaultValue="9.99" step="any" type={INPUT_TYPE.number} />
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Name:
      </FormFieldLabel>

      <Input />
    </FormField>
}
```

### Loading

```tsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input loading />
}
```

### Masked

```tsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input maskOption={{
    enable: true
  }} />
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Input placeholder="Input" />
}
```

### Read Only

```tsx
{
  globals: {
    imports: `import { Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Input defaultValue="Readonly" readOnly />
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    alignItems: 'flex-start'
  }}>
      <Input placeholder="Default" />
      <Input clearable defaultValue="Clearable" />
      <Input loading placeholder="Loading" />
      <Input disabled placeholder="Disabled" />
      <Input invalid placeholder="Invalid" />
      <Input readOnly defaultValue="Read only" />
      <Input clearable maskOption={{
      enable: true
    }} defaultValue="Clearable" />
    </div>
}
```

### Types

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px',
    alignItems: 'start'
  }}>{story()}</div>],
  globals: {
    imports: `import { INPUT_TYPE, Input } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Input type={INPUT_TYPE.email} placeholder="email" /><br />
      <Input type={INPUT_TYPE.number} placeholder="number" /><br />
      <Input type={INPUT_TYPE.password} placeholder="password" /><br />
      <Input type={INPUT_TYPE.search} placeholder="search" /><br />
      <Input type={INPUT_TYPE.text} placeholder="text" /><br />
      <Input type={INPUT_TYPE.time} placeholder="time" /><br />
      <Input type={INPUT_TYPE.url} placeholder="url" /><br />
    </>
}
```

## React Components

# Kbd

The **Kbd** component is used to display keyboard keys or key combinations, typically for shortcuts or user instructions. It visually distinguishes key inputs from regular text.

Esc

## Overview

---

**Kbd** is a non-interactive component designed to represent keyboard keys or combinations. The purpose of the **Kbd** is to provide a standardized way to format and present key inputs.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Kbd</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/kbd" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Kbd** is commonly used to display single keys (e.g., Esc) and keyboard shortcuts (e.g., ⌘ + S, Ctrl + Shift + P) in:

-   Documentation
-   Interactive guides
-   Accessibility section

This component is not interactive and should not be used to trigger any action.

### Dos & Don'ts

| ✅ Do |
| --- |
| Keep combinations consistent with platform conventions (e.g., ⌘ for macOS, Ctrl for Windows/Linux) |
| Keep separator usage consistent (using `+`) |
| Integrate Kbd inline with instructional text (e.g., `Press <kbd>Ctrl</kbd> + <kbd>S</kbd> to save your changes.` |

| ❌ Don't |
| --- |
| Use Kbd as a substitute for clickable button. It must not trigger any action |
| Nest Kbd inside other interactive elements |
| Use Kbd for decorative text or to stylize regular words |
| Add multiple keys into one element |
| Display unrealistic or platform-inconsistent combinations |
| Use it for icons or non-keyboard symbols |

### Best Practices in Context

1.  **Kbd**

## Behavior

---

**Kbd** has no interactive behavior. It is a purely visual component.

The **Kbd** component adapts to its parent font size.

## Navigation

---

The **Kbd** component is not focusable by default, as it is non-interactive.

## Accessibility

---

This component complies with the [Kbd HTML element](https://developer.mozilla.org/fr/docs/Web/HTML/Reference/Elements/kbd) .

## React Components

# Kbd

## Overview

---

Esc

## Anatomy

---

Kbd

---

Esc

## Kbd

---

This component has no specific properties.

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-kbd-background-color | var(--ods-color-neutral-100) | 
 |
| --ods-kbd-border-bottom-width | calc(var(--ods-theme-border-width) * 2) | 

 |
| --ods-kbd-border-color | var(--ods-color-neutral-200) | 

 |
| --ods-kbd-border-radius | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-kbd-border-width | var(--ods-theme-border-width) | 

 |
| --ods-kbd-padding-horizontal | calc(var(--ods-theme-padding-horizontal) / 2) | 

 |
| --ods-kbd-padding-vertical | calc(var(--ods-theme-padding-vertical) / 4) | 

 |
| --ods-kbd-text-color | var(--ods-color-neutral-700) | 

 |

## Examples

---

### Default

Cmd

```jsx
<Kbd>
  Cmd
</Kbd>
```

## Recipes

---

No recipe defined for now.

## React Components/Kbd

## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Kbd>Esc</Kbd>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Kbd } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Kbd>Cmd</Kbd>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    children: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    }
  }),
  args: {
    children: 'Cmd + L'
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Kbd>Esc</Kbd>
}
```

### Theme Generator

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Kbd>Cmd</Kbd>
}
```

## React Components

# Link

_**Link** is a component that enables redirection to a new page, section, website or other resources:_

[Link](https://www.ovhcloud.com)

## Overview

---

A **Link** allows users to move through a website by redirecting them to a different page or section.

It can also enables **Links** to other resources.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Link</td></tr><tr><th scope="row">Also known as</th><td>Hyperlink, Anchor</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=44-6837" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/link" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-link--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Links** are used as navigational elements.

They are also used when linking to another document. In that case, **Link** description should contain type and size of document.

### When to use a link or a button?

Links and buttons serve different purposes, and using them correctly improves both accessibility and user experience.

-   Use a link when the action navigates the user to another page or resource, either within your application or to an external site
-   Use a button when the action performs a function or triggers a behavior on the current page, like submitting a form, opening a modal, or toggling a menu.

A button should not mimic a link. It can lead to confusion for users and assistive technologies, as buttons are not typically expected to handle navigation.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a link to navigate to another page, section, or external website, not to trigger actions |
| - Make link labels clear and descriptive, so users understand where the link will take them |
| - Keep link labels short ideally 1–4 words |
| - Indicate when a link leads to a document or file by including its type and size (e.g., "Annual Report (PDF, 1.2MB)") |
| - Optionally include an icon, but ensure it's styled consistently with the link label |

| ❌ Don't |
| --- |
| - Don't use a link to perform actions like "save", "submit", or "cancel", use a button for that |
| - Don't use vague text like "Click here" or "Read more", it provides no context for screen readers or SEO |
| - Don't write entire sentences as link label |
| - Don't apply different colors to a link and its adjacent icon. They should appear as one cohesive element |
| - Don't open a link in a new tab or window unless it's clearly indicated and necessary (e.g., for external sites) |
| - Don't forget to set appropriate rel attributes (e.g., rel="noopener noreferrer") for external links opened in new tabs |

### Best Practices in Context

1.  **Link**
2.  **Label** - optional
3.  **Icon** - optional (left or right)

## Placement

---

A **Link** can stand alone on a page, or it can be placed within a sentence or a paragraph.

## Behavior

---

**Link** component can be hovered, focused, clicked and disabled.

When disabled, users cannot focus nor click on the **Link**.

On hover, **Link** is underlined and its color changes.

An optional icon can be displayed on the left or right of the **Link** label content. Icon-only **Link** also exists but it must meet accessibility requirements. See Accessibility section below.

When users clicks anywhere on the **Link**, even its optional icon, they are redirected to the expected page or section.

## Navigation

---

### Focus Management

The **Link** component can receive keyboard focus and is part of the standard tab order.

### General Keyboard Shortcuts

Pressing Enter while a **Link** is focused activates it, navigating to the associated destination.

Pressing Shift + Tab moves focus to the previous interactive element.

If a **Link** opens in a new tab, focus remains on the original page unless otherwise specified.

## Accessibility

---

This component complies with the [Link WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/link/) .

### Icon-only Link

If a **Link** is represented only by an **Icon**, it must have an explicit accessible label to provide context about its destination or action.

[​](https://www.ovhcloud.com)

```jsx
<Link
  aria-label="Go to homepage"
  href="https://www.ovhcloud.com"
>
  <Icon name="home" />
</Link>
```

Screen readers will announce the link and its label.

### Link opening in a new tab

When a **Link** opens in a new tab (`target="_blank"`), users should be informed to avoid confusion. This can be done by adding a visual indicator (e.g., Icon: "external") and using `aria-label` to provide context.

[​](https://www.ovhcloud.com)

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Link aria-label="Visit Example (opens in a new tab)" href="https://www.ovhcloud.com" target="_blank">
      <Icon name={ICON_NAME.externalLink} />
    </Link>
}
```

The screen reader will announce the link, the opening in a new tab behavior and the link label.

### Downloadable files

If a **Link** points to a file download, it should include the file type and size information.

[​](https://www.w3.org/TR/2024/REC-WCAG21-20241212.pdf)[Download WCAG20 Guidelines (PDF, 481 KB)](https://www.w3.org/TR/2024/REC-WCAG21-20241212.pdf)

```jsx
{
  decorators: [story => <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Icon, Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Link aria-label="Download WCAG20 Guidelines (PDF, 481 KB)" href="https://www.w3.org/TR/2024/REC-WCAG21-20241212.pdf">
        <Icon name={ICON_NAME.download} />
      </Link>
      <Link href="https://www.w3.org/TR/2024/REC-WCAG21-20241212.pdf">
        <Icon name={ICON_NAME.download} />
        <span>Download WCAG20 Guidelines (PDF, 481 KB)</span>
      </Link>
    </>
}
```

Screen readers will announce the link, the link label with the file name, type and size, ensuring users know they are downloading a file.

## React Components

# Link

## Overview

---

## Anatomy

---

Link

---

[Link](https://www.ovhcloud.com)

## Link

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [a attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#attributes) . |
| 
as

 | `<T>` | - | `'a'` | Pass a component you may want to use as custom Link component. Useful for example when using routing library like react-router. |
| 

disabled

 | `boolean` | - | `false` | Whether the component is disabled. |

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-link-column-gap | var(--ods-theme-column-gap) | 
 |
| --ods-link-row-gap | var(--ods-theme-row-gap) | 

 |

## Examples

---

### Default

[Default Link](https://www.ovhcloud.com)

```jsx
{
  globals: {
    imports: `import { Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Link href="https://www.ovhcloud.com">
      Default Link    </Link>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Link disabled href="https://www.ovhcloud.com">
      Disabled    </Link>
}
```

### With Icon

```jsx
{
  decorators: [story => <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 1fr'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Icon, Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Link href="https://www.ovhcloud.com">
        <Icon name={ICON_NAME.arrowLeft} />Icon Link
      </Link>
      <Link href="https://www.ovhcloud.com" style={{
      justifySelf: 'right'
    }}>
        Icon Link<Icon name={ICON_NAME.arrowRight} />
      </Link>
    </>
}
```

### React Router integration

Using the `as` attribute, you can use the **Link** component with external libraries like [React Router](https://reactrouter.com/) .

```typescript
import { Link as OdsLink } from '@ovhcloud/ods-react';
import { Link as RouterLink } from 'react-router-dom';
const Link = ({ children, route }) => {
  return (
    <OdsLink
      as={ RouterLink }
      to={ route }>
      { children }
    </OdsLink>
  );
};
export { Link };
```

## Recipes

---

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Feature List Product Card

WEB HOSTING

NewBest seller

Performance

For demanding online stores and projects.

1 vCore 2,4 GHz, 2 Go RAM1 vCore 2,4 GHz, 4 Go RAM2 vCores 2,4 GHz, 8 Go RAM

From

€24.46ex. VAT/month

or €13.19 incl. VAT/monthfor a 24-month registration

Minimum 2-year registration €100 free with a 5-year registration

Installation fee:Free

-   -   Unlimited websites
        
    -   High power level
        
    -   1 domain name free for the first
        
    -   500 GB SSD storage
        
    -   1,000 email addresses
        
-   1-click CMS
    
    -   WordPress
        
    -   Joomla!
        
    -   Drupal
        
    -   Prestashop
        
-   Database
    
    -   4 x 1 GB databases
        
    -   8 GB Web Cloud Databases
        
-   Security
    
    -   Unlimited free SSL
        
    -   Anti-DDoS protection
        
    -   Anti-virus and anti-spam
        
    -   Daily backups
        
-   Performance
    
    -   99.9% observed availability
        
    -   Guaranteed resources
        
    -   Unlimited traffic
        
    -   Service continuity
        
    -   Boost option to withstand temporary traffic spikes
        
-   Support and additional services
    
    -   Git
        
    -   Standard support
        
    -   SSH access
        
    -   CDN Basic
        

Media Product Card

AI Deploy

Easily deploy machine learning models and applications into production, create your API access points with ease, and make effective predictions.

## React Components/Link

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `` | No |  | @default-value='a' Pass a component you may want to use as custom Link component. Useful for example when using routing library like react-router. |
| `disabled` | `` | No | false | Whether the component is disabled. |


## Examples


### Accessibility File Download

```tsx
{
  decorators: [story => <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Icon, Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Link aria-label="Download WCAG20 Guidelines (PDF, 481 KB)" href="https://www.w3.org/TR/2024/REC-WCAG21-20241212.pdf">
        <Icon name={ICON_NAME.download} />
      </Link>

      <Link href="https://www.w3.org/TR/2024/REC-WCAG21-20241212.pdf">
        <Icon name={ICON_NAME.download} />

        <span>Download WCAG20 Guidelines (PDF, 481 KB)</span>
      </Link>
    </>
}
```

### Accessibility Icon Only Link

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Link aria-label="Go to homepage" href="https://www.ovhcloud.com">
      <Icon name={ICON_NAME.home} />
    </Link>
}
```

### Accessibility In A New Tab

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Link aria-label="Visit Example (opens in a new tab)" href="https://www.ovhcloud.com" target="_blank">
      <Icon name={ICON_NAME.externalLink} />
    </Link>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Link href="https://www.ovhcloud.com" target="_blank">
      Link
    </Link>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Link href="https://www.ovhcloud.com">
      Default Link
    </Link>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    children: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    }
  }),
  args: {
    // @ts-ignore check when time to do so
    children: 'My link'
  }
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Link disabled href="https://www.ovhcloud.com">
      Disabled
    </Link>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Link href="https://www.ovhcloud.com" target="_blank">
      Link
    </Link>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>
      <Link href="https://www.ovhcloud.com">Default Link</Link>
      <Link disabled href="https://www.ovhcloud.com">Disabled</Link>
      <Link href="https://www.ovhcloud.com"><Icon name={ICON_NAME.arrowLeft} />Icon Left</Link>
      <Link href="https://www.ovhcloud.com" style={{
      justifySelf: 'right'
    }}>Icon Right<Icon name={ICON_NAME.arrowRight} /></Link>
    </div>
}
```

### With Icon

```tsx
{
  decorators: [story => <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 1fr'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Icon, Link } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Link href="https://www.ovhcloud.com">
        <Icon name={ICON_NAME.arrowLeft} />Icon Link
      </Link>

      <Link href="https://www.ovhcloud.com" style={{
      justifySelf: 'right'
    }}>
        Icon Link<Icon name={ICON_NAME.arrowRight} />
      </Link>
    </>
}
```

## React Components

# Logo

## Overview

---

The **Logo** component is a visual representation of the official brand.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Logo</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/LC3NLyBG7rTBB4z2BrgFtx/ODS---UI-Kit?node-id=14404-2175&amp;t=hWEYBdYcCp9xXYYY-0" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/logo" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

Use the **Logo** component to display the brand identity in key areas of the interface, such as headers, navigation bars, footers, or branded sections.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the Logo component to represent the brand identity consistently across your interfaces |
| - Choose the appropriate variant based on available space and hierarchy |
| - Ensure adequate spacing around the logo so it remains clearly visible and legible |

| ❌ Don't |
| --- |
| - Do not change logo colors outside of the approved brand versions |
| - Do not use the Logo as an interactive element (button or link) unless explicitly wrapped by a dedicated navigation component |
| - Do not overcrowd the logo with surrounding content |

## Placement

---

The **Logo** component can be used in various layouts, including:

-   Application headers and top navigation bars
-   Sidebars
-   Footers
-   Splash screens or landing pages
-   Branded sections within a page

## Variation

---

### Variant

#### Emblem

Displays the logo emblem only. Best suited for compact spaces (e.g. collapsed navigation, mobile headers, favicon-like usage).

#### Default

Displays the logo emblem and wordmark horizontally.

Recommended for standard headers and navigation bars where horizontal space is available.

### Size

#### Small

Use in constrained spaces such as mobile headers, compact navigation bars, or secondary branding areas.

#### Medium

Default choice for standard application headers and footers.

#### Large

Use for high-visibility contexts such as landing pages, splash screens, or branded hero sections.

## Navigation

---

The **Logo** component is non-interactive and does not receive keyboard focus. It is used solely for displaying the brand identity and does not impact keyboard navigation.

If the logo needs to act as a link (e.g. redirect to the homepage), it must be wrapped in a separate interactive element that handles navigation and focus.

## Accessibility

---

When the logo is used within a navigational element (for example, as a link in a header), an `aria-label` on the link should reflect the intended action, such as:

-   "Navigate to the homepage"
-   "Return to the [Brand name] home page"

[](https://www.ovhcloud.com)

```jsx
<a
  aria-label="Return to the OVHcloud home page"
  href="https://www.ovhcloud.com"
>
  <Logo />
</a>
```

Screen readers will announce the `aria-label`.

## React Components

# Logo

## Overview

---

## Anatomy

---

Logo

---

## Logo

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [svg attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/svg#attributes) . |
| 
size

 | `LOGO_SIZE` | - | `LOGO_SIZE.md` | The size preset to use. |
| 

variant

 | `LOGO_VARIANT` | - | `LOGO_VARIANT.default` | The variant preset to use. |

## Enums

---

### LOGO_SIZE

-   lg =`"lg"`
-   md =`"md"`
-   sm =`"sm"`

### LOGO_VARIANT

-   default =`"default"`
-   emblem =`"emblem"`

## Examples

---

### Default

```jsx
<Logo />
```

### Size

```jsx
{
  decorators: [story => <div style={{
    display: 'grid',
    rowGap: '16px'
  }}>{story()}</div>],
  globals: {
    imports: `import { LOGO_SIZE, Logo } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Logo size={LOGO_SIZE.sm} />
      <Logo size={LOGO_SIZE.md} />
      <Logo size={LOGO_SIZE.lg} />
    </>
}
```

### Variant

```jsx
<>
  <Logo variant="default" />
  <Logo variant="emblem" />
</>
```

## Recipes

---

No recipe defined for now.

## React Components/Logo

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `size` | `` | No | LOGO_SIZE.md | The size preset to use. |
| `variant` | `` | No | LOGO_VARIANT.default | The variant preset to use. |


## Examples


### Accessibility Link

```tsx
{
  globals: {
    imports: `import { Logo } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <a aria-label="Return to the OVHcloud home page" href="https://www.ovhcloud.com">
      <Logo />
    </a>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <div data-ods="logo-story">
      <Logo />
    </div>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Logo } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Logo />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    size: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'LOGO_SIZE'
        }
      },
      control: {
        type: 'select'
      },
      options: LOGO_SIZES
    },
    variant: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'LOGO_VARIANT'
        }
      },
      control: {
        type: 'select'
      },
      options: LOGO_VARIANTS
    }
  })
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Logo />
}
```

### Size

```tsx
{
  decorators: [story => <div style={{
    display: 'grid',
    rowGap: '16px'
  }}>{story()}</div>],
  globals: {
    imports: `import { LOGO_SIZE, Logo } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Logo size={LOGO_SIZE.sm} />
      <Logo size={LOGO_SIZE.md} />
      <Logo size={LOGO_SIZE.lg} />
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <Logo />
}
```

### Variant

```tsx
{
  decorators: [story => <div style={{
    display: 'grid',
    rowGap: '16px'
  }}>{story()}</div>],
  globals: {
    imports: `import { LOGO_VARIANT, Logo } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Logo variant={LOGO_VARIANT.default} />
      <Logo variant={LOGO_VARIANT.emblem} />
    </>
}
```

## React Components

# Markdown

# Readme

Wanna know more about **OVHCloud Design System**?

Check the [Documentation](https://ovh.github.io/design-system/).

# Readme Wanna know more about **OVHCloud Design System**? Check the [Documentation](https://ovh.github.io/design-system/).

## Overview

---

**Markdown** component is a versatile tool for rendering Markdown syntax into a visually appealing and user-friendly format. It transforms Markdown elements into corresponding ODS components, ensuring consistency and accessibility.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Markdown</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=0-1&amp;p=f&amp;t=kwInPIiCg5eOsgBa-0" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/markdown" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Markdown** component is used to parse Markdown content and convert it in a readable and engaging way. It is ideal for rendering user-generated content, documentation, and other types of text that require formatting and styling.

## Placement

---

**Markdown** component can be used in various contexts, such as articles, blogs, forums, and documentation pages. It can be placed in a container or used as a standalone element, depending on the desired layout and design.

## Behavior

---

**Markdown** component automatically parses and renders Markdown syntax, including headings, text formatting, links, code blocks, and other elements. It also supports customization and overrides for individual Markdown elements, allowing developers to adapt the rendering to specific use cases.

## Navigation

---

**Markdown** component does not have any navigational elements, as it is primarily used for rendering static content.

## Accessibility

---

**Markdown** accessibility follows what components they are formatting the text into.

## React Components

# Markdown

## Overview

---

# Readme

Wanna know more about **OVHCloud Design System**?

Check the [Documentation](https://ovh.github.io/design-system/).

# Readme Wanna know more about **OVHCloud Design System**? Check the [Documentation](https://ovh.github.io/design-system/).

## Anatomy

---

Markdown

---

## Markdown

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
component

 | `Record<keyof JSX.IntrinsicElements, ReactNode>` | - | `undefined` | Override of default component rendering |
| 

content

 | `string` |  | `undefined` | The Markdown content to transform. |

## Examples

---

### Default

# Heading 1

## Heading 2

### Heading 3

#### Heading 4

##### Heading 5

###### Heading 6

> Blockquotes

`Inline code`

```
// print something in the console
console.log('Hello World!');
```

| Column 1 | Column 2 |
| --- | --- |
| Value 1 | Value 2 |

```jsx
{
  globals: {
    imports: `import { Markdown } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Markdown content={`# Heading 1
  ## Heading 2  ### Heading 3  #### Heading 4  ##### Heading 5  ###### Heading 6  > Blockquotes
  \`Inline code\`  \`\`\`  // print something in the console  console.log('Hello World!');  \`\`\`
  | Column 1 | Column 2 |  | --- | --- |  | Value 1 | Value 2 |  `} />
}
```

### Custom Component

You can use your own component to render some of the Markdown content.

The `component` attribute is a Map that expects as key any HTML tag that are usually rendered from Markdown (h1, a, img, ...).

Your renderer will be given as props the native attributes you'll normally received from the Markdown (like `href` for a link, `src`, `alt` and `title` for an image, ...).

Here is an example with a [target blank link](https://ovh.github.io/design-system/)

```jsx
<Markdown component={{
  a: ({
    children,
    href  }) => <Link href={href} target="_blank">{children}</Link>
}} content="Here is an example with a [target blank link](https://ovh.github.io/design-system/)" />
```

### Code Highlighter

```typescript
import { Code, Markdown } from '@ovhcloud/ods-react';
import lang from '@shikijs/langs/typescript';
import theme from '@shikijs/themes/nord';
const HighlightedCode = () => (
  <Markdown
    component={{
      code: ({ children }) => {
        return (
          <Code
            highlighter={{
              language: lang,
              theme: theme,
            }}>
            { children as string }
          </Code>
        );
      }
    }}
    content={`# Highlighted Code
```
// print something in the console
console.log('Hello World!');
```
`} />
);
```

## Recipes

---

No recipe defined for now.

## React Components/Markdown

## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <div data-ods="markdown-story">
      <Markdown content={`# Readme
  Wanna know more about **OVHCloud Design System**?

  Check the [Documentation](https://ovh.github.io/design-system/).`} />
    </div>
}
```

### Code Highlighter

```tsx
{
  globals: {
    imports: `import { Code, Markdown } from '@ovhcloud/ods-react';
import lang from '@shikijs/langs/typescript';
import theme from '@shikijs/themes/nord';`
  },
  tags: ['!dev'],
  render: ({}) => <Markdown component={{
    code: ({
      children
    }) => {
      return <Code highlighter={{
        language: lang,
        theme: theme
      }}>
              {children as string}
            </Code>;
    }
  }} content={`# Highlighted Code
  \`\`\`
  // print something in the console
  console.log('Hello World!');
  \`\`\`
  `} />
}
```

### Custom Component

```tsx
{
  globals: {
    imports: `import { Link, Markdown } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <Markdown component={{
    a: ({
      children,
      href
    }) => <Link href={href} target="_blank">{children}</Link>
  }} content="Here is an example with a [target blank link](https://ovh.github.io/design-system/)" />
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Markdown } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Markdown content={`# Heading 1
  ## Heading 2
  ### Heading 3
  #### Heading 4
  ##### Heading 5
  ###### Heading 6
  > Blockquotes

  \`Inline code\`
  \`\`\`
  // print something in the console
  console.log('Hello World!');
  \`\`\`

  | Column 1 | Column 2 |
  | --- | --- |
  | Value 1 | Value 2 |
  `} />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    content: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    }
  }),
  args: {
    content: 'Markdown **bold** content'
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => {
    const content = `# Readme
Wanna know more about **OVHCloud Design System**?

Check the [Documentation](https://ovh.github.io/design-system/).`;
    return <Tabs defaultValue="preview" style={{
      height: '240px',
      width: '350px'
    }} variant={TABS_VARIANT.switch}>
        <TabList>
          <Tab value="preview">Preview</Tab>
          <Tab value="source">Source</Tab>
        </TabList>

        <TabContent value="preview">
          <Markdown content={content} />
        </TabContent>

        <TabContent value="source">
          <Text preset={TEXT_PRESET.paragraph} style={{
          whiteSpace: 'break-spaces'
        }}>
            {content}
          </Text>
        </TabContent>
      </Tabs>;
  }
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <Markdown content={`# Heading 1
  ## Heading 2
  ### Heading 3
  #### Heading 4
  ##### Heading 5
  ###### Heading 6
  > Blockquotes

  \`Inline code\`
  \`\`\`
  // print something in the console
  console.log('Hello World!');
  \`\`\`

  | Column 1 | Column 2 |
  | --- | --- |
  | Value 1 | Value 2 |
  `} />
}
```

## React Components

# Medium

## Overview

---

The **Medium** component is a versatile UI element designed to handle different types of media content.

It ensures that media is displayed correctly and consistently within the application.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Medium</td></tr><tr><th scope="row">Also known as</th><td>Media, Image</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=237-13496" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/medium" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-medium--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Medium** is used to display assets with an alternative text description.

When the asset is an image, it should be mostly presentation to draw user's attention visually.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the Medium component to display images and media consistently across the application |
| - Provide alt text, captions, or descriptions where appropriate, to ensure accessibility and context |
| - Ensure responsive behavior so medium adapts well to different screen sizes |

| ❌ Don't |
| --- |
| - Don't use Medium with embedded text that cannot be read by screen readers |
| - Don't overload pages with high-resolution medium that may slow down load times |
| - Don't stretch or distort Medium to fit containers, maintain proportions and visual quality |
| - Don't rely on Medium alone to convey essential information. Always provide a textual equivalent when needed |

## Placement

---

The width and height dimensions of the image can be set to fixed sizes.

## Navigation

---

The **Medium** component is non-interactive and does not receive keyboard focus. It is used solely for displaying media content and does not impact keyboard navigation.

## Accessibility

---

### Use caption text to add information about an image

Caption is useful to add information about the image itself (ex: copyright), whereas the alternative text always describe what the image actually conveys.

© Copyright National Aeronautics and Space Administration

```jsx
<figure>
  <Medium
    alt="NASA Computer room"
    src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg"
  />
  <figcaption>
    <Text preset="caption">
      © Copyright National Aeronautics and Space Administration    </Text>
  </figcaption>
</figure>
```

## React Components

# Medium

## Overview

---

## Anatomy

---

Medium

---

## Medium

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [img attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes) . |

## Examples

---

### Default

```jsx
<Medium
  alt="NASA Computer room"
  src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg"
/>
```

### Height

```jsx
{
  globals: {
    imports: `import { Medium } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Medium alt="NASA Computer room" height={40} src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg" />
}
```

### Width

```jsx
{
  globals: {
    imports: `import { Medium } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Medium alt="NASA Computer room" src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg" width={300} />
}
```

## Recipes

---

Media Product Card

AI Deploy

Easily deploy machine learning models and applications into production, create your API access points with ease, and make effective predictions.

## React Components/Medium

## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Medium alt="NASA Computer room" height={100} src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg" />
}
```

### Caption

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, Medium, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <figure>
      <Medium alt="NASA Computer room" src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg" />

      <figcaption>
        <Text preset={TEXT_PRESET.caption}>
          © Copyright National Aeronautics and Space Administration
        </Text>
      </figcaption>
    </figure>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Medium } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Medium alt="NASA Computer room" src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg" />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    height: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'number'
        }
      },
      control: 'number'
    },
    src: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    width: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'number'
        }
      },
      control: 'number'
    }
  }),
  args: {
    src: 'https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg'
  }
}
```

### Height

```tsx
{
  globals: {
    imports: `import { Medium } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Medium alt="NASA Computer room" height={40} src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg" />
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Medium alt="NASA Computer room" height={100} src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg" />
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <Medium alt="NASA Computer room" height={40} src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg" />
}
```

### Width

```tsx
{
  globals: {
    imports: `import { Medium } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Medium alt="NASA Computer room" src="https://upload.wikimedia.org/wikipedia/commons/b/b9/NASAComputerRoom7090.NARA.jpg" width={300} />
}
```

## React Components

# Menu

_A Menu component provides a list of options or actions that appear when a user interacts with its trigger element._

## Overview

---

The Menu component allows users to access related actions through a compact, organized list. It may inclue simple or grouped actions.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Menu</td></tr><tr><th scope="row">Also known as</th><td>Dropdown Menu, Context Menu, Options/Actions Menu</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/ViIEmUvAGU3kR346yKSDXv/ODS---UI-Kit?m=auto&amp;node-id=14267-46263&amp;t=9NEhUC6EmfGbc7Fc-1" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/menu" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The Menu component is commonly used for:

-   User account options (profile, settings, logout)
-   Contextual actions in tables or list (edit, duplicate, delete)
-   Organizing multiple secondary actions under one trigger (e.g., "More options")

### Dos & Don'ts

| ✅ Do |
| --- |
| - Group related actions under headings when the list is long |
| - Use separators when needed to clarify structure between distinct actions |
| - Ensure the most frequently used actions are near the top |

| ❌ Don't |
| --- |
| - Don’t use a Menu as primary navigation structure |
| - Don't overload a Menu with too many unrelated actions |
| - Don't place destructive actions (e.g., "Delete") without clear distinction or confirmation |

### Best Practices in Context

Menu Trigger Menu content Group - optional Group title - optional Item

1.  **Menu**
2.  **Trigger**
3.  **Menu content**
4.  **Group** - optional
5.  **Group title** - optional
6.  **Item**

## Behavior

---

The Menu opens when the trigger element is clicked. The list of items will be displayed.

Selecting an item triggers its associated action and closes the Menu.

Clicking outside closes the Menu.

### Disabling

Disabled items cannot be selected, or interact with.

### Grouping

Heading

An optional label above a group of related items is displayed.

Separator / Divider

A separator is displayed between the groups or between items.

### Nested Menus

The Menu supports nested sub-menus with a maximum depth of one level. A menu item may expose a sub-menu, indicated visually with a right-facing arrow. Sub-menu opens when the user hovers over the parent menu item. Closing the Menu also closes any open sub-menu.

## Navigation

---

### Focus Management

The Menu trigger can receive keyboard focus and is part of the standard tab order.

When the Menu opens, the first enabled item is focused.

Focus is trapped within the Menu until it is closed.

If the Menu closes, focus returns to the trigger element.

### General Keyboard Shortcuts

Pressing `Arrow Up / Down` navigates through items.

Pressing `Enter` or `Space` activates the focused item.

Pressing `Arrow Left` or `Escape` closes a sub-menu or the Menu.

Pressing `Arrow Right` opens a sub-menu (when relevant)

Pressing `Tab`, or `Shift` + `Tab`, exits the Menu and moves to the next/previous focusable element

## Accessibility

---

This component complies with the [Menu WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) .

## React Components

# Menu

## Overview

---

## Anatomy

---

Menu

MenuContent

MenuGroup

MenuGroupLabel

MenuItem

MenuSubmenu

MenuTrigger

---

## Menu

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
onOpenChange

 | `(detail: MenuOpenChangeDetail) => void` | - | `undefined` | Callback fired when the menu open state changes. |
| 

onPositionChange

 | `(detail: MenuPositionChangeDetail) => void` | - | `undefined` | Callback fired when the menu position changes. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the menu. |
| 

overlayConfig

 | `object` | - | `undefined` | The overlay configuration. |
| 

flip

 | `boolean` | - | `-` | Whether to flip the position. |
| 

gutter

 | `number` | - | `-` | The main axis offset or gap between the reference and floating elements. |
| 

position

 | `MENU_POSITION` | - | `-` | The menu position around the trigger. |
| 

sameWidth

 | `boolean` | - | `-` | Whether to make the floating element same width as the reference element. |
| 

positionDeprecated

 | `MENU_POSITION` | - | `undefined` | Moved to overlayConfig. |
| 

positionerStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |
| 

triggerId

 | `string` | - | `undefined` | ID of an external trigger element to use in place of the MenuTrigger component. |

## MenuContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. This only works on the root menu and not in submenus. |
| 

withArrow

 | `boolean` | - | `false` | Whether the component displays an arrow pointing to the trigger. |

## MenuGroup

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## MenuGroupLabel

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## MenuItem

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
disabled

 | `boolean` | - | `undefined` | Whether the item is disabled. |
| 

onSelect

 | `() => void` | - | `undefined` | Callback fired when the selection changes. |
| 

value

 | `string` |  | `undefined` | The value of the item. |

## MenuSubmenu

---

This component has no specific properties.

## MenuTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## Interfaces

---

### MenuOpenChangeDetail

-   `open: boolean`

### MenuPositionChangeDetail

-   `position: MENU_POSITION`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Button, Menu, MenuContent, MenuItem, MenuTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
          Open menu        </Button>
      </MenuTrigger>
      <MenuContent>
        <MenuItem value="profile">Profile</MenuItem>
        <MenuItem value="settings">Settings</MenuItem>
        <MenuItem value='logout'>Logout</MenuItem>
      </MenuContent>
    </Menu>
}
```

### Controlled

```jsx
const [isOpen, setIsOpen] = useState(false);
  function toggleMenu() {
    setIsOpen(isOpen => !isOpen);
  }
  return <>
      <Button onClick={toggleMenu}>
        Toggle menu      </Button>
      <Menu open={isOpen}>
        <MenuTrigger asChild>
          <Icon name={ICON_NAME.ellipsisVertical} />
        </MenuTrigger>
        <MenuContent>
          <MenuItem value='profile'>Profile</MenuItem>
          <MenuItem value='settings'>Settings</MenuItem>
          <MenuItem value='logout'>Logout</MenuItem>
        </MenuContent>
      </Menu>
    </>;
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Button, Menu, MenuContent, MenuItem, MenuTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
          Open menu        </Button>
      </MenuTrigger>
      <MenuContent>
        <MenuItem value="profile">Profile</MenuItem>
        <MenuItem disabled value="settings">Settings</MenuItem>
        <MenuItem value="logout">Logout</MenuItem>
      </MenuContent>
    </Menu>
}
```

### Group

```jsx
{
  globals: {
    imports: `import { Button, Menu, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
          Open menu        </Button>
      </MenuTrigger>
      <MenuContent>
        <MenuGroup>
          <MenuGroupLabel>Account</MenuGroupLabel>
          <MenuItem value='profile'>Profile</MenuItem>
          <MenuItem value='billing'>Billing</MenuItem>
        </MenuGroup>
        <MenuGroup>
          <MenuGroupLabel>Support</MenuGroupLabel>
          <MenuItem value='help'>Help center</MenuItem>
          <MenuItem value='contact'>Contact support</MenuItem>
        </MenuGroup>
      </MenuContent>
    </Menu>
}
```

### Nested

```jsx
{
  globals: {
    imports: `import { Button, Menu, MenuContent, MenuItem, MenuSubmenu, MenuTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
          Open menu        </Button>
      </MenuTrigger>
      <MenuContent>
        <MenuItem value='new'>New</MenuItem>
        <MenuItem value='open'>Open</MenuItem>
        <MenuSubmenu>
          <MenuTrigger>Share</MenuTrigger>
          <MenuContent>
            <MenuItem value='email'>Email</MenuItem>
            <MenuItem value='message'>Message</MenuItem>
          </MenuContent>
        </MenuSubmenu>
      </MenuContent>
    </Menu>
}
```

## Recipes

---

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

## React Components/Menu

## Subcomponents


### MenuContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. This only works on the root menu and not in submenus. |
| `withArrow` | `` | No | false | Whether the component displays an arrow pointing to the trigger. |



### MenuGroup



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `asChild` | `` | No |  | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |



### MenuGroupLabel



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `asChild` | `` | No |  | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |



### MenuItem



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `disabled` | `` | No |  | Whether the item is disabled. |
| `onSelect` | `` | No |  | Callback fired when the selection changes. |
| `value` | `` | Yes |  | The value of the item. |



### MenuTrigger



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `asChild` | `` | No |  | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |


## Examples


### Anatomy Tech

```tsx
{
  parameters: {
    layout: 'start'
  },
  tags: ['!dev'],
  render: ({}) => <Menu open overlayConfig={{
    flip: false
  }}>
      <MenuTrigger asChild>
        <Button>
          Menu trigger
        </Button>
      </MenuTrigger>

      <MenuContent createPortal={false}>
        <MenuGroup>
          <MenuGroupLabel>My account</MenuGroupLabel>
          <MenuItem value='profile'>Profile</MenuItem>
          <MenuItem value='settings'>Settings</MenuItem>
        </MenuGroup>
        <MenuItem value='logout'>Logout</MenuItem>
      </MenuContent>
    </Menu>
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { Button, ICON_NAME, Icon, Menu, MenuContent, MenuItem, MenuTrigger } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const [isOpen, setIsOpen] = useState(false);
    function toggleMenu() {
      setIsOpen(isOpen => !isOpen);
    }
    return <>
        <Button onClick={toggleMenu}>
          Toggle menu
        </Button>

        <Menu open={isOpen}>
          <MenuTrigger asChild>
            <Icon name={ICON_NAME.ellipsisVertical} />
          </MenuTrigger>
          <MenuContent>
            <MenuItem value='profile'>Profile</MenuItem>
            <MenuItem value='settings'>Settings</MenuItem>
            <MenuItem value='logout'>Logout</MenuItem>
          </MenuContent>
        </Menu>
      </>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Button, Menu, MenuContent, MenuItem, MenuTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
          Open menu
        </Button>
      </MenuTrigger>
      <MenuContent>
        <MenuItem value="profile">Profile</MenuItem>
        <MenuItem value="settings">Settings</MenuItem>
        <MenuItem value='logout'>Logout</MenuItem>
      </MenuContent>
    </Menu>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    gutter: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'number'
        }
      },
      control: 'number'
    },
    position: {
      control: 'select',
      options: MENU_POSITIONS,
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'MENU_POSITION'
        }
      }
    },
    sameWidth: {
      table: {
        category: CONTROL_CATEGORY.design
      },
      control: {
        type: 'boolean'
      }
    },
    triggerLabel: {
      control: 'text',
      table: {
        category: CONTROL_CATEGORY.slot,
        type: {
          summary: 'string'
        }
      }
    },
    withArrow: {
      control: 'boolean',
      table: {
        category: CONTROL_CATEGORY.design
      }
    }
  }),
  args: {
    position: MENU_POSITION.bottom,
    triggerLabel: 'Open menu',
    withArrow: false
  },
  render: (arg: Partial<DemoArg>) => <Menu overlayConfig={{
    gutter: arg.gutter,
    position: arg.position,
    sameWidth: arg.sameWidth
  }}>
      <MenuTrigger asChild>
        <Button>
          {arg.triggerLabel}
        </Button>
      </MenuTrigger>

      <MenuContent withArrow={arg.withArrow}>
        <MenuItem value="profile">Profile</MenuItem>
        <MenuItem value="settings">Settings</MenuItem>
      </MenuContent>
    </Menu>
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Button, Menu, MenuContent, MenuItem, MenuTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
          Open menu
        </Button>
      </MenuTrigger>
      <MenuContent>
        <MenuItem value="profile">Profile</MenuItem>
        <MenuItem disabled value="settings">Settings</MenuItem>
        <MenuItem value="logout">Logout</MenuItem>
      </MenuContent>
    </Menu>
}
```

### Group

```tsx
{
  globals: {
    imports: `import { Button, Menu, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
          Open menu
        </Button>
      </MenuTrigger>
      <MenuContent>
        <MenuGroup>
          <MenuGroupLabel>Account</MenuGroupLabel>
          <MenuItem value='profile'>Profile</MenuItem>
          <MenuItem value='billing'>Billing</MenuItem>
        </MenuGroup>
        <MenuGroup>
          <MenuGroupLabel>Support</MenuGroupLabel>
          <MenuItem value='help'>Help center</MenuItem>
          <MenuItem value='contact'>Contact support</MenuItem>
        </MenuGroup>
      </MenuContent>
    </Menu>
}
```

### Nested

```tsx
{
  globals: {
    imports: `import { Button, Menu, MenuContent, MenuItem, MenuSubmenu, MenuTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
          Open menu
        </Button>
      </MenuTrigger>
      <MenuContent>
        <MenuItem value='new'>New</MenuItem>
        <MenuItem value='open'>Open</MenuItem>
        <MenuSubmenu>
          <MenuTrigger>Share</MenuTrigger>
          <MenuContent>
            <MenuItem value='email'>Email</MenuItem>
            <MenuItem value='message'>Message</MenuItem>
          </MenuContent>
        </MenuSubmenu>
      </MenuContent>
    </Menu>
}
```

### Overview

```tsx
{
  parameters: {
    layout: 'centered'
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
          Account
        </Button>
      </MenuTrigger>
      <MenuContent>
        <MenuGroup>
          <MenuGroupLabel>My account</MenuGroupLabel>
          <MenuItem value='profile'>Profile</MenuItem>
          <MenuItem value='settings'>Settings</MenuItem>
        </MenuGroup>
        <MenuItem value='logout'>Logout</MenuItem>
      </MenuContent>
    </Menu>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <Menu>
      <MenuTrigger asChild>
        <Button>
            Default
        </Button>
      </MenuTrigger>
      <MenuContent createPortal={false}>
        <MenuItem value='profile'>Profile</MenuItem>
        <MenuItem value='settings'>Settings</MenuItem>
      </MenuContent>
    </Menu>
}
```

## React Components

# Message

_**Message** component helps to communicate with users providing feedback or information._

Message

## Overview

---

**Message** component communicates information to the user.

It grabs the user's attention in a prominent way.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Message</td></tr><tr><th scope="row">Also known as</th><td>Notifier, Notification, Banner, Alert, Feedback</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=44-8300" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/message" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-message--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Messages** are used as notification status.

There are four different subtypes of standard **Messages**, in order of priority:

-   **Information**: Provides info to users in context. Shouldn't be overused to replace regular content.
-   **Success**: Reserved to provide to a success message. They are displayed as a snackbar.
-   **Warning**: Reserved for **Messages** that need the user attention and acknowledgment but might not cause errors.
-   **Critical**: Reserved for errors, malfunctions, as well as critical issues. Inform the user that a problem requires immediate intervention or correction before the process continues.

### Message vs Toast

**Message**

-   Persistent or semi-persistent information banner.
-   Usually appears inline or at the top of a page as part of the content flow.
-   Designed for status communication (success, warning, critical error, info) that’s tied to a specific screen or context.
-   Remains visible until dismissed or until the state changes.

**Toast**

-   Ephemeral notification that appears for a short time (usually overlay in a corner of the viewport)
-   Designed for lightweight, transient feedback ("Action succeeded", "File uploaded"), except for toast with actions.
-   Auto-dismisses after a few seconds.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Write Message in a clear, concise, and affirmative tone that aligns with the user's task |
| - Place Message in a visible and relevant area of the page, ideally close to the triggering element if any or top of the section |
| - Use the correct variant (e.g., information, success, warning, error) to match the context and urgency |
| - Use non-dismissible Message for critical warnings or errors that require immediate resolution |
| - Allow content selection (e.g., to copy error messages) |

| ❌ Don't |
| --- |
| - Don't use Message to highlight static or decorative content, prefer components like Card for that |
| - Don't write overly long or complex Message that could overwhelm or confuse users |
| - Don't rely solely on color to convey meaning, always keep the Icon for clarity |
| - Don't use vague or generic text, always provide helpful context or guidance |
| - Don't overload the interface with multiple simultaneous Messages unless absolutely necessary |

### Best Practices in Context

1.  **Message**
2.  **Icon**
3.  **Close button** - optional
4.  **Content**

## Placement

---

**Messages** show up during a task to notify users of the status of an action. In every page, it is possible to display one or more **Messages** to inform the user.

They are positioned near their related items or right after the header and before the page title and page content.

## Behavior

---

Based on its informational manner, the component default behavior is being read-only. You can only select its content if needed.

When a **Message** is dismissible, a click on the `xmark` icon button will dismiss the **Message**.

## Variation

---

### Color

-   **Information** _(default)_: informing users of specific content, providing global feedback to the user.
-   **Success**: confirming that an action has been completed successfully, providing positive feedback to the user.
-   **Warning**: alerting users to potential issues or cautionary information, prompting them to take preventive actions.
-   **Critical**: highlighting severe and potentially catastrophic issues that demand urgent and decisive action to prevent significant negative consequences.

### Variant

-   **Default**: conveying mild to important information, ensuring the message is seen by the user.
-   **Light**: conveying non-urgent, informational content in a subtle and less prominent manner, ensuring the message is seen without drawing too much attention.

## Navigation

---

### Focus Management

The **Message** component itself is non-interactive and does not receive keyboard focus.

If the **Message** is dismissible, the close icon button is focusable.

### General Keyboard Shortcuts

Pressing Tab moves focus to the close icon button (if available).

Pressing Enter or Space while the close icon button is focused should trigger the action to dismiss the **Message**.

Pressing Shift + Tab moves focus to the previous interactive element.

## Accessibility

---

### Structuring multiple Messages

When displaying multiple **Messages** together, they should be wrapped within an unordered list of items (`<ul>` and `<li>`) to ensure a proper announcement by screen readers.

-   Your changes have been saved.
    
-   Some fields need your attention.
    

```jsx
{
  globals: {
    imports: `import { ICON_NAME, MESSAGE_COLOR, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ul style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px',
    margin: 0,
    padding: 0,
    listStyle: 'none'
  }}>
      <li>
        <Message>
          <MessageIcon name={ICON_NAME.circleCheck} />
          <MessageBody>
            Your changes have been saved.          </MessageBody>
        </Message>
      </li>
      <li>
        <Message color={MESSAGE_COLOR.warning}>
          <MessageIcon name={ICON_NAME.triangleExclamation} />
          <MessageBody>
            Some fields need your attention.          </MessageBody>
        </Message>
      </li>
    </ul>
}
```

This structure ensures that assistive technologies announce **Messages** as a list, rather than reading them as separate, unrelated announcements.

Screen readers will announce the list, the number of items and the **Messages** content.

### Alternative approach with ARIA roles

When modifying the HTML structure is not possible, use [role="list"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/list_role) and [role="listitem"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/listitem_role) to mimic list semantics.

Your changes have been saved.

Some fields need your attention.

```jsx
{
  globals: {
    imports: `import { ICON_NAME, MESSAGE_COLOR, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <div role="list" style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>
      <Message role="listitem">
        <MessageIcon name={ICON_NAME.circleCheck} />
        <MessageBody>
          Your changes have been saved.        </MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.warning} role="listitem">
        <MessageIcon name={ICON_NAME.triangleExclamation} />
        <MessageBody>
          Some fields need your attention.        </MessageBody>
      </Message>
    </div>
}
```

### Setting the container ARIA role

When adding a new **Message** element, its container should have a `role` attribute set, so that assistive technologies can announce the content correctly.

If you're adding a critical **Message**, use `role="alert"`. For non-critical **Messages** (e.g., info, success, warning), use `role="status"`.

```jsx
const [alerts, setAlerts] = useState<string[]>([]);
  const [statuses, setStatuses] = useState<string[]>([]);
  return <>
      <div>
        <Button onClick={() => setStatuses(s => s.concat([new Date().toString()]))}>
          Add status        </Button>
        <Button color={BUTTON_COLOR.critical} onClick={() => setAlerts(a => a.concat([new Date().toString()]))}>
          Add alert        </Button>
      </div>
      <div role="alert">
        {alerts.map(alert => <Message color={MESSAGE_COLOR.critical} key={alert}>
              <MessageIcon name={ICON_NAME.hexagonExclamation} />
              <MessageBody>
                Alert: {alert}
              </MessageBody>
            </Message>)}
      </div>
      <div role="status">
        {statuses.map(status => <Message key={status}>
              <MessageIcon name={ICON_NAME.circleInfo} />
              <MessageBody>
                Status: {status}
              </MessageBody>
            </Message>)}
      </div>
    </>;
}
```

This ensures that screen readers announce the **Message** at the appropriate time without interrupting.

## React Components

# Message

## Overview

---

## Anatomy

---

Message

MessageBody

MessageIcon

---

## Message

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
color

 | `MESSAGE_COLOR` | - | `MESSAGE_COLOR.information` | The color preset to use. |
| 

dismissible

 | `boolean` | - | `true` | Whether the remove button is displayed. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

onRemove

 | `() => void` | - | `undefined` | Callback fired when the close button is pressed. |
| 

variant

 | `MESSAGE_VARIANT` | - | `MESSAGE_VARIANT.default` | The variant preset to use. |

## MessageBody

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## MessageIcon

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |
| 
name

 | `ICON_NAME` |  | `undefined` | The icon name. |

## Enums

---

### MESSAGE_COLOR

-   critical =`"critical"`
-   information =`"information"`
-   neutral =`"neutral"`
-   primary =`"primary"`
-   success =`"success"`
-   warning =`"warning"`

### MESSAGE_I18N

-   closeButton =`"message.close.button"`

### MESSAGE_VARIANT

-   default =`"default"`
-   light =`"light"`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-message-background-color-critical | var(--ods-color-critical-075) | 
 |
| --ods-message-background-color-critical-light | var(--ods-theme-background-color) | 

 |
| --ods-message-background-color-information | var(--ods-color-information-075) | 

 |
| --ods-message-background-color-information-light | var(--ods-theme-background-color) | 

 |
| --ods-message-background-color-neutral | var(--ods-color-neutral-075) | 

 |
| --ods-message-background-color-neutral-light | var(--ods-theme-background-color) | 

 |
| --ods-message-background-color-primary | var(--ods-color-primary-500) | 

 |
| --ods-message-background-color-primary-light | var(--ods-theme-background-color) | 

 |
| --ods-message-background-color-success | var(--ods-color-success-075) | 

 |
| --ods-message-background-color-success-light | var(--ods-theme-background-color) | 

 |
| --ods-message-background-color-warning | var(--ods-color-warning-075) | 

 |
| --ods-message-background-color-warning-light | var(--ods-theme-background-color) | 

 |
| --ods-message-border-radius | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-message-close-button-text-color-primary | var(--ods-color-neutral-000) | 

 |
| --ods-message-text-color-critical | var(--ods-color-critical-700) | 

 |
| --ods-message-text-color-critical-light | var(--ods-color-critical-600) | 

 |
| --ods-message-text-color-information | var(--ods-color-information-700) | 

 |
| --ods-message-text-color-information-light | var(--ods-color-information-600) | 

 |
| --ods-message-text-color-neutral | var(--ods-color-neutral-700) | 

 |
| --ods-message-text-color-neutral-light | var(--ods-color-neutral-600) | 

 |
| --ods-message-text-color-primary | var(--ods-color-primary-000) | 

 |
| --ods-message-text-color-primary-light | var(--ods-color-primary-600) | 

 |
| --ods-message-text-color-success | var(--ods-color-success-700) | 

 |
| --ods-message-text-color-success-light | var(--ods-color-success-600) | 

 |
| --ods-message-text-color-warning | var(--ods-color-warning-700) | 

 |
| --ods-message-text-color-warning-light | var(--ods-color-warning-600) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Message>
      <MessageIcon name={ICON_NAME.circleInfo} />
      <MessageBody>
        Default message      </MessageBody>
    </Message>
}
```

### Color

```jsx
{
  decorators: [story => <div style={{
    display: 'inline-flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, MESSAGE_COLOR, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Message color={MESSAGE_COLOR.critical}>
        <MessageIcon name={ICON_NAME.hexagonExclamation} />
        <MessageBody>Critical message</MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.information}>
        <MessageIcon name={ICON_NAME.circleInfo} />
        <MessageBody>Information message</MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.neutral}>
        <MessageIcon name={ICON_NAME.email} />
        <MessageBody>Neutral message</MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.primary}>
        <MessageIcon name={ICON_NAME.lightbulb} />
        <MessageBody>Primary message</MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.success}>
        <MessageIcon name={ICON_NAME.circleCheck} />
        <MessageBody>Success message</MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.warning}>
        <MessageIcon name={ICON_NAME.triangleExclamation} />
        <MessageBody>Warning message</MessageBody>
      </Message>
    </>
}
```

### Variant

```jsx
{
  decorators: [story => <div style={{
    display: 'inline-flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, MESSAGE_VARIANT, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Message variant={MESSAGE_VARIANT.default}>
        <MessageIcon name={ICON_NAME.circleInfo} />
        <MessageBody>
          Default variant Message        </MessageBody>
      </Message>
      <Message variant={MESSAGE_VARIANT.light}>
        <MessageIcon name={ICON_NAME.circleInfo} />
        <MessageBody>
          Light variant Message        </MessageBody>
      </Message>
    </>
}
```

### Multiline

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Message>
      <MessageIcon name={ICON_NAME.circleInfo} />
      <MessageBody>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer faucibus, libero et pharetra mattis, ipsum velit semper risus, non ultrices lacus massa sed arcu. Nulla sed tellus.      </MessageBody>
    </Message>
}
```

### Non Dismissible

```jsx
<Message dismissible={false}>
  <MessageIcon name={ICON_NAME.circleInfo} />
  <MessageBody>
    Default message  </MessageBody>
</Message>
```

## Recipes

---

Status Message

Activate your project and get €200 in free cloud credit

You are currently in discovery mode. Activate your project to unlock your cloud resources and start building in minutes.

## React Components

# Message Bubble

_The **Message Bubble** component is designed to display messages in a conversational interface._

Hello, how can I help you?

## Overview

---

The **Message Bubble** component is designed to display messages in a conversational interface.

It can be used in various contexts, such as customer support chats, messaging applications, or even internal communication tools within an organization.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Message Bubble</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="http://localhost/TODO" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/message-bubble" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Message Bubble** component is used to display individual messages within a conversation or chat interface.

Common usages include:

-   Displaying user-generated text messages in a chat window
-   Showing system messages, like "Conversation started" or "User is typing...", to provide context and feedback to users
-   Using the component in a conversational AI interface to display AI-generated responses, complete with typing indicators and support for structured content

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use within a conversational interface or a similar context where messages are exchanged between users or between a user and an AI assistant |
| - Leverage the sender information features (Avatar, sender name) to provide clear context about who sent the message |
| - Apply distinct visual treatments for different sender types (User, AI assistant, System) to enhance user understanding and engagement |

| ❌ Don't |
| --- |
| - Don't use the Message Bubble component as a replacement for a text input field; it's meant for displaying messages, not for user input |
| - Don't use the component in a way that could lead to confusion about the sender's identity or the message's content |

### Best Practices in Context

1.  **Message Bubble**
2.  **Content**

## Behavior

---

The component supports several states that reflect its current condition or the status of the message:

-   **Typing**: Indicates that the message is being either typed by a human or generated by an AI assistant.
    
-   **Error**: Displays an error state when message delivery fails, providing visual feedback to the user

## React Components

# Message Bubble

## Overview

---

Hello, how can I help you?

## Anatomy

---

MessageBubble

---

## MessageBubble

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
error

 | `boolean` | - | `false` | Indicates an error state |
| 

typing

 | `boolean` | - | `false` | When typing a message |
| 

variant

 | `MESSAGE_BUBBLE_VARIANT` | - | `MESSAGE_BUBBLE_VARIANT.human` | Visual variant of the message bubble |

## Enums

---

### MESSAGE_BUBBLE_VARIANT

-   ai =`"ai"`
-   human =`"human"`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-message-bubble-ai-background-color | var(--ods-color-information-025) | 
 |
| --ods-message-bubble-background-color | var(--ods-message-bubble-error-background-color) | 

 |
| --ods-message-bubble-border-radius | calc(var(--ods-theme-border-radius) * 2) | 

 |
| --ods-message-bubble-error-background-color | var(--ods-color-critical-025) | 

 |
| --ods-message-bubble-error-text-color | var(--ods-color-critical-500) | 

 |
| --ods-message-bubble-human-background-color | var(--ods-color-neutral-050) | 

 |
| --ods-message-bubble-padding | calc(var(--ods-theme-padding-vertical) * 2) calc(var(--ods-theme-padding-horizontal) * 2) | 

 |
| --ods-message-bubble-text-color | var(--ods-message-bubble-error-text-color) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { MessageBubble } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <MessageBubble>
      Hello, how can I help you?    </MessageBubble>
}
```

### Variant

This is a human message

This is an AI message

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { MESSAGE_BUBBLE_VARIANT, MessageBubble } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <MessageBubble variant={MESSAGE_BUBBLE_VARIANT.human}>
        This is a human message      </MessageBubble>
      <MessageBubble variant={MESSAGE_BUBBLE_VARIANT.ai}>
        This is an AI message      </MessageBubble>
    </>
}
```

### Error

```jsx
{
  globals: {
    imports: `import { MessageBubble } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <MessageBubble error>
      An error occurred while generating the response.    </MessageBubble>
}
```

### Typing

```jsx
{
  globals: {
    imports: `import { MessageBubble } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <MessageBubble typing />
}
```

## Recipes

---

Chat

Assistant2:58 PM

Welcome to the Chat recipe. Feel free to test the UI behavior by typing anything on your mind below.

## React Components/Message Bubble

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `error` | `` | No | false | Indicates an error state |
| `typing` | `` | No | false | When typing a message |
| `variant` | `` | No | MESSAGE_BUBBLE_VARIANT.human | Visual variant of the message bubble |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <MessageBubble>
      Hello, how can I help you?
    </MessageBubble>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { MessageBubble } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <MessageBubble>
      Hello, how can I help you?
    </MessageBubble>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    children: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    error: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    typing: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    variant: {
      table: {
        category: CONTROL_CATEGORY.design,
        defaultValue: {
          summary: MESSAGE_BUBBLE_VARIANT.human
        },
        type: {
          summary: 'MESSAGE_BUBBLE_VARIANT'
        }
      },
      control: {
        type: 'select'
      },
      options: MESSAGE_BUBBLE_VARIANTS
    }
  }),
  args: {
    children: 'Hello, how can I help you?'
  }
}
```

### Error

```tsx
{
  globals: {
    imports: `import { MessageBubble } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <MessageBubble error>
      An error occurred while generating the response.
    </MessageBubble>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <MessageBubble>
      Hello, how can I help you?
    </MessageBubble>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '8px'
  }}>
      {MESSAGE_BUBBLE_VARIANTS.map(variant => <div key={variant} style={{
      display: 'flex',
      flexDirection: 'column',
      gap: '8px'
    }}>
          <MessageBubble variant={variant}>
            {String(variant)} message
          </MessageBubble>
          <MessageBubble variant={variant} error>
            {String(variant)} error message
          </MessageBubble>
          <MessageBubble variant={variant} typing />
        </div>)}
    </div>
}
```

### Typing

```tsx
{
  globals: {
    imports: `import { MessageBubble } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <MessageBubble typing />
}
```

### Variant

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { MESSAGE_BUBBLE_VARIANT, MessageBubble } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <MessageBubble variant={MESSAGE_BUBBLE_VARIANT.human}>
        This is a human message
      </MessageBubble>
      <MessageBubble variant={MESSAGE_BUBBLE_VARIANT.ai}>
        This is an AI message
      </MessageBubble>
    </>
}
```

## React Components/Message

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No | MESSAGE_COLOR.information | @type=MESSAGE_COLOR The color preset to use. |
| `dismissible` | `` | No | true | Whether the remove button is displayed. |
| `i18n` | `` | No |  | Internal translations override. |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `onRemove` | `` | No |  | Callback fired when the close button is pressed. |
| `variant` | `` | No | MESSAGE_VARIANT.default | The variant preset to use. |


## Subcomponents


### MessageBody




### MessageIcon



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `name` | `` | Yes |  | The icon name. |


## Examples


### Accessibility Alternative Grouping

```tsx
{
  globals: {
    imports: `import { ICON_NAME, MESSAGE_COLOR, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <div role="list" style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>
      <Message role="listitem">
        <MessageIcon name={ICON_NAME.circleCheck} />

        <MessageBody>
          Your changes have been saved.
        </MessageBody>
      </Message>

      <Message color={MESSAGE_COLOR.warning} role="listitem">
        <MessageIcon name={ICON_NAME.triangleExclamation} />

        <MessageBody>
          Some fields need your attention.
        </MessageBody>
      </Message>
    </div>
}
```

### Accessibility Grouping

```tsx
{
  globals: {
    imports: `import { ICON_NAME, MESSAGE_COLOR, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ul style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px',
    margin: 0,
    padding: 0,
    listStyle: 'none'
  }}>
      <li>
        <Message>
          <MessageIcon name={ICON_NAME.circleCheck} />

          <MessageBody>
            Your changes have been saved.
          </MessageBody>
        </Message>
      </li>

      <li>
        <Message color={MESSAGE_COLOR.warning}>
          <MessageIcon name={ICON_NAME.triangleExclamation} />

          <MessageBody>
            Some fields need your attention.
          </MessageBody>
        </Message>
      </li>
    </ul>
}
```

### Accessibility Roles

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { BUTTON_COLOR, ICON_NAME, MESSAGE_COLOR, Button, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [alerts, setAlerts] = useState<string[]>([]);
    const [statuses, setStatuses] = useState<string[]>([]);
    return <>
        <div>
          <Button onClick={() => setStatuses(s => s.concat([new Date().toString()]))}>
            Add status
          </Button>

          <Button color={BUTTON_COLOR.critical} onClick={() => setAlerts(a => a.concat([new Date().toString()]))}>
            Add alert
          </Button>
        </div>

        <div role="alert">
          {alerts.map(alert => <Message color={MESSAGE_COLOR.critical} key={alert}>
                <MessageIcon name={ICON_NAME.hexagonExclamation} />

                <MessageBody>
                  Alert: {alert}
                </MessageBody>
              </Message>)}
        </div>

        <div role="status">
          {statuses.map(status => <Message key={status}>
                <MessageIcon name={ICON_NAME.circleInfo} />

                <MessageBody>
                  Status: {status}
                </MessageBody>
              </Message>)}
        </div>
      </>;
  }
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Message>
      <MessageIcon name={ICON_NAME.circleInfo} />

      <MessageBody>
        Message
      </MessageBody>
    </Message>
}
```

### Color

```tsx
{
  decorators: [story => <div style={{
    display: 'inline-flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, MESSAGE_COLOR, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Message color={MESSAGE_COLOR.critical}>
        <MessageIcon name={ICON_NAME.hexagonExclamation} />

        <MessageBody>Critical message</MessageBody>
      </Message>

      <Message color={MESSAGE_COLOR.information}>
        <MessageIcon name={ICON_NAME.circleInfo} />

        <MessageBody>Information message</MessageBody>
      </Message>

      <Message color={MESSAGE_COLOR.neutral}>
        <MessageIcon name={ICON_NAME.email} />

        <MessageBody>Neutral message</MessageBody>
      </Message>

      <Message color={MESSAGE_COLOR.primary}>
        <MessageIcon name={ICON_NAME.lightbulb} />

        <MessageBody>Primary message</MessageBody>
      </Message>

      <Message color={MESSAGE_COLOR.success}>
        <MessageIcon name={ICON_NAME.circleCheck} />

        <MessageBody>Success message</MessageBody>
      </Message>

      <Message color={MESSAGE_COLOR.warning}>
        <MessageIcon name={ICON_NAME.triangleExclamation} />

        <MessageBody>Warning message</MessageBody>
      </Message>
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Message>
      <MessageIcon name={ICON_NAME.circleInfo} />

      <MessageBody>
        Default message
      </MessageBody>
    </Message>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Message color={arg.color} dismissible={arg.dismissible} variant={arg.variant}>
      <MessageIcon name={arg.name || ICON_NAME.circleInfo} />

      <MessageBody>
        {arg.children}
      </MessageBody>
    </Message>,
  argTypes: orderControls({
    children: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    color: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'MESSAGE_COLOR'
        }
      },
      control: {
        type: 'select'
      },
      options: MESSAGE_COLORS
    },
    dismissible: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    name: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'ICON_NAME'
        }
      },
      control: {
        type: 'select'
      },
      options: ICON_NAMES
    },
    variant: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'MESSAGE_VARIANT'
        }
      },
      control: {
        type: 'select'
      },
      options: MESSAGE_VARIANTS
    }
  }),
  args: {
    children: 'My message'
  }
}
```

### Multiline

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Message>
      <MessageIcon name={ICON_NAME.circleInfo} />

      <MessageBody>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer faucibus, libero et pharetra mattis, ipsum velit semper risus, non ultrices lacus massa sed arcu. Nulla sed tellus.
      </MessageBody>
    </Message>
}
```

### Non Dismissible

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <Message dismissible={false}>
      <MessageIcon name={ICON_NAME.circleInfo} />

      <MessageBody>
        Default message
      </MessageBody>
    </Message>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Message>
      <MessageIcon name={ICON_NAME.circleInfo} />

      <MessageBody>
        Message
      </MessageBody>
    </Message>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'inline-flex',
    flexFlow: 'column',
    gap: '8px'
  }}>
      <Message>
        <MessageIcon name={ICON_NAME.circleInfo} />
        <MessageBody>Default message</MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.information}>
        <MessageIcon name={ICON_NAME.circleInfo} />
        <MessageBody>Information</MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.success}>
        <MessageIcon name={ICON_NAME.circleCheck} />
        <MessageBody>Success</MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.warning}>
        <MessageIcon name={ICON_NAME.triangleExclamation} />
        <MessageBody>Warning</MessageBody>
      </Message>
      <Message color={MESSAGE_COLOR.critical}>
        <MessageIcon name={ICON_NAME.hexagonExclamation} />
        <MessageBody>Critical</MessageBody>
      </Message>
      <Message variant={MESSAGE_VARIANT.light}>
        <MessageIcon name={ICON_NAME.circleInfo} />
        <MessageBody>Light variant</MessageBody>
      </Message>
    </div>
}
```

### Variant

```tsx
{
  decorators: [story => <div style={{
    display: 'inline-flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, MESSAGE_VARIANT, Message, MessageBody, MessageIcon } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Message variant={MESSAGE_VARIANT.default}>
        <MessageIcon name={ICON_NAME.circleInfo} />

        <MessageBody>
          Default variant Message
        </MessageBody>
      </Message>

      <Message variant={MESSAGE_VARIANT.light}>
        <MessageIcon name={ICON_NAME.circleInfo} />

        <MessageBody>
          Light variant Message
        </MessageBody>
      </Message>
    </>
}
```

## React Components

# Meter

## Overview

---

The **Meter** component visually represents a quantitative value within a defined range.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Meter</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/YdqCQ0e4CtGBm7dLbuLl1h/ODS---UI-Kit?node-id=10494-5637&amp;t=9pIeofapdgMDhhF4-0" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/meter" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Meter** component represents a quantitative value such as resource usage, storage availability, or other bounded metrics. It is distinct from a progress bar due to its focus on measurement and thresholds rather than progress toward a goal.

Additionally, the **Meter** is primarily static, representing a fixed state or measurement, whereas the progress bar is dynamic, reflecting continuous updates or progress over time.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the Meter to represent bounded measurements, such as disk usage or CPU load |
| - Customize thresholds and colors to provide meaningful visual cues |
| - Include appropriate labels when context is not immediately clear |

| ❌ Don't |
| --- |
| Don't use a Meter to represent unbounded progress (use a progress bar instead) |
| Avoid separating the Meter and its associated value across different sections of the UI to prevent confusion |
| Avoid excessive customization that compromises readability or accessibility |

### Best Practices in Context

1.  **Meter**
2.  **Track**
3.  **Fill**

## Placement

---

The placement of the **Meter** depends on its use case and the context within the UI.

Place the **Meter** close to the element it represents (e.g., near a resource title).

If the **Meter** is tied to a specific numeric value (e.g., "75% used"), display the value nearby for clarity, directly adjacent to the component.

Include appropriate labels when context is not immediately clear:

-   inline: labels can be placed above, below or beside the Meter.
-   surrounding: labels for minimum and maximum values can be positioned at opposite ends of the track, as needed.
-   hidden: for visual-only representation.

## Behavior

---

Value representation supports minimum, maximum, and current value inputs. The filled portion of the **Meter** is dynamically adjusted based on the value.

Low and high thresholds can be specified to visually indicate important ranges. Color will change based on those thresholds.

The coloring behavior may be updated through the optimum attribute. See the [optimum documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meter#optimum) for more information.

## Navigation

---

The **Meter** component is non-interactive and does not receive keyboard focus.

## Accessibility

---

To ensure proper accessibility, **Meter** must be correctly labeled.

### Always provide an explicit label

Every **Meter** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either an [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) or an [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby) attribute.

```jsx
<Meter
  aria-label="Gauge"
  low={40}
  value={35}
/>
```

Screen readers will announce the label and the meter information.

Gauge:

```jsx
<>
  <Text
    id="meter-label"
    preset="label"
  >
    Gauge:  </Text>
  <Meter
    aria-labelledby="meter-label"
    low={40}
    value={35}
  />
</>
```

Screen readers will announce the label and the meter information.

### Improve value readability

Numbers aren't always user-friendly. You can use the [aria-valuetext](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-valuetext) attribute to present the current value in a more user-friendly, human-understandable way.

```jsx
<Meter
  aria-label="Gauge"
  aria-valuetext="35 files uploaded"
  low={40}
  value={35}
/>
```

Screen readers will announce the label and the meter information in a more user-friendly, human-understandable way.

## React Components

# Meter

## Overview

---

## Anatomy

---

Meter

---

## Meter

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
high

 | `number` | - | `undefined` | The lower numeric bound of the high end of the measured range. |
| 

low

 | `number` | - | `undefined` | The upper numeric bound of the low end of the measured range. |
| 

max

 | `number` | - | `100` | The upper numeric bound of the measured range. |
| 

min

 | `number` | - | `0` | The lower numeric bound of the measured range. |
| 

optimum

 | `number` | - | `undefined` | The optimal numeric value. Combined with low and high, it will changes the coloring behaviour. |
| 

value

 | `number` | - | `0` | The current value of the meter |

## Unions

---

-   `MeterState = "critical" | "normal" | "warning"`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-meter-background-color-critical | var(--ods-theme-critical-color) | 
 |
| --ods-meter-background-color-normal | var(--ods-theme-primary-color) | 

 |
| --ods-meter-background-color-warning | var(--ods-theme-warning-color) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Meter } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Meter />
}
```

### Thresholds

```jsx
{
  globals: {
    imports: `import { Meter } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Value under low threshold:</p>
      <Meter low={40} value={35} />
      <p>Value between both thresholds:</p>
      <Meter high={80} low={40} value={60} />
      <p>Value above high threshold:</p>
      <Meter high={80} value={90} />
    </>
}
```

### Optimum

```jsx
{
  globals: {
    imports: `import { Meter } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Low optimum and low value:</p>
      <Meter high={80} low={40} optimum={30} value={20} />
      <p>Low optimum and high value:</p>
      <Meter high={80} low={40} optimum={30} value={60} />
      <p>Low optimum and very high value:</p>
      <Meter high={80} low={40} optimum={30} value={90} />
    </>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Meter

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `high` | `` | No |  | The lower numeric bound of the high end of the measured range. |
| `low` | `` | No |  | The upper numeric bound of the low end of the measured range. |
| `max` | `` | No | 100 | The upper numeric bound of the measured range. |
| `min` | `` | No | 0 | The lower numeric bound of the measured range. |
| `optimum` | `` | No |  | The optimal numeric value. Combined with low and high, it will changes the coloring behaviour. |
| `value` | `` | No | 0 | The current value of the meter |


## Examples


### Accessibility Aria Label

```tsx
{
  globals: {
    imports: `import { Meter } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Meter aria-label="Gauge" low={40} value={35} />
}
```

### Accessibility Aria Labelledby

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, Meter, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Text id="meter-label" preset={TEXT_PRESET.label}>
        Gauge:
      </Text>

      <Meter aria-labelledby="meter-label" low={40} value={35} />
    </>
}
```

### Accessibility Aria Valuetext

```tsx
{
  globals: {
    imports: `import { Meter } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Meter aria-label="Gauge" aria-valuetext="35 files uploaded" low={40} value={35} />
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <div style={{
    width: '200px'
  }}>
      <Meter low={40} value={35} />
    </div>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Meter } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Meter />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    high: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    low: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    max: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    min: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    optimum: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    value: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    }
  })
}
```

### Optimum

```tsx
{
  globals: {
    imports: `import { Meter } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Low optimum and low value:</p>
      <Meter high={80} low={40} optimum={30} value={20} />

      <p>Low optimum and high value:</p>
      <Meter high={80} low={40} optimum={30} value={60} />

      <p>Low optimum and very high value:</p>
      <Meter high={80} low={40} optimum={30} value={90} />
    </>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Meter low={40} value={35} />
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px'
  }}>
      <Meter />
      <Meter low={40} value={35} />
      <Meter high={80} low={40} value={60} />
      <Meter high={80} value={90} />
      <Meter optimum={30} value={20} />
    </div>
}
```

### Thresholds

```tsx
{
  globals: {
    imports: `import { Meter } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Value under low threshold:</p>
      <Meter low={40} value={35} />

      <p>Value between both thresholds:</p>
      <Meter high={80} low={40} value={60} />

      <p>Value above high threshold:</p>
      <Meter high={80} value={90} />
    </>
}
```

## React Components

# Modal

_A **Modal** component is used to overlay with important content the main view of a site and block interactions with it._

## Overview

---

A **Modal** is used to provide some information to users that needs a response or immediate attention.

While a **Modal** is triggered, there is no interaction possible on the page that is overlaid so users have to dismiss or click on an action Button to proceed further.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Modal</td></tr><tr><th scope="row">Also known as</th><td>Dialog, Alert Dialog, Confirm Dialog</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=47-2607" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/modal" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-modal--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Modals** are used in different cases:

-   alerting users about something that requires their agreement
-   confirming a user decision
-   notifying the user of important information

They can be dismissable or not.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Modal to capture user attention for critical decisions, confirmations, or required input that blocks the current flow |
| - Ensure the Modal has a clear and focused purpose, with concise messaging and a single main action |
| - Allow the Modal to be dismissed via a cancel button, close icon, or Escape key, unless a decision is mandatory |
| - Keep Modal content short and scannable. Support scroll only when necessary |

| ❌ Don't |
| --- |
| - Don't use Modals for non-interruptive information, prefer inline content or a Message component for passive feedback |
| - Don't trigger a second Modal from within another Modal since this leads to confusion and poor accessibility |
| - Don't use Modals for long-form content like documentation, complex forms, or multiple paragraphs. Consider a dedicated page or Drawer instead |
| - Don't make a Modal dismissible if the user must take an action to proceed and ensure the intent is clear |
| - Don't overload the Modal with multiple CTAs (Call-to-Action) or overly complex UI elements |

### Best Practices in Context

1.  **Modal**
2.  **Header**
3.  **Close button** - optional
4.  **Content**
5.  **Title & description**
6.  **Actions**

## Placement

---

When a **Modal** opening is triggered, it is displayed with a position aligned both horizontally and vertically within the viewport.

**Modal** takes up 100% of the viewport width up to a maximum width of 512px.

On smaller screens, the **Modal** takes all the available space with a margin of 16px.

## Behavior

---

### Overlay

A background overlay with opacity is displayed on the page to avoid distraction and help users to focus on the **Modal** content.

An animation with the ease-in and out effect applies on opening.

### Closing

A **Modal** is dismissed by clicking on the close icon button.

Dismissing a **Modal** means that it will be closed without submitting any data and the user won't proceed further.

A **Modal** will be successfully closed only once the required action or response has been completed by the user, meaning that the task is completed so the user will proceed further.

The hidden overflow behavior is removed whenever the **Modal** get closed.

### Scrolling

When necessary, depending on the viewport height and the **Modal** content, the user can scroll vertically with the header and the action button remaining in place.

## Navigation

---

### Focus Management

When the **Modal** is opened, focus is automatically set to the **Modal** itself.

Focus is trapped within the **Modal** and its inner elements while it remains open.

When the **Modal** is closed, focus returns to the trigger element unless otherwise specified.

### General Keyboard Shortcuts

Pressing Escape closes the **Modal** (if dismissible)

Pressing Tab moves focus forward:

-   First to the close icon button (if available)
-   Then through any other focusable elements inside the **Modal** (e.g., buttons, inputs)

Pressing Shift + Tab moves focus backward through the focusable elements.

Once the last focusable element is reached, focus loops back to the first focusable element, ensuring it remains within the **Modal**.

## Accessibility

---

This component complies with the [Modal WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) .

### Linking the Modal title and content

Ensure that assistive technologies announce the **Modal** correctly using [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby) or [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) , and [aria-describedby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-describedby) .

```jsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal        </Button>
      </ModalTrigger>
      <ModalContent aria-describedby="modal-content" aria-label="Modal Content">
        <ModalHeader>Modal header</ModalHeader>
        <ModalBody id="modal-content">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.        </ModalBody>
      </ModalContent>
    </Modal>
}
```

```jsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal        </Button>
      </ModalTrigger>
      <ModalContent aria-describedby="modal-content" aria-labelledby="modal-title">
        <ModalHeader><span id="modal-title">Delete item</span></ModalHeader>
        <ModalBody>
          <p id="modal-content">
            Are you sure you want to delete this item? This action cannot be undone.          </p>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

Screen readers will announce the sections referenced by the aria attributes.

### Prioritize the least critical action in modal buttons

For **Modals** that require user decisions (e.g., confirmation dialogs), place the least critical **Button** first in the **Modal** actions section.

```jsx
{
  globals: {
    imports: `import { BUTTON_COLOR, BUTTON_VARIANT, MODAL_COLOR, Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal        </Button>
      </ModalTrigger>
      <ModalContent color={MODAL_COLOR.critical}>
        <ModalHeader><span id="modal-title">Delete item</span></ModalHeader>
        <ModalBody>
          <p id="modal-content">
            Are you sure you want to delete this item?          </p>
          <div style={{
          display: 'flex',
          gap: '8px',
          justifyContent: 'end'
        }}>
            <Button variant={BUTTON_VARIANT.ghost}>
              Cancel            </Button>
            <Button color={BUTTON_COLOR.critical}>
              Delete            </Button>
          </div>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

Users will first encounter the non-destructive action (e.g., "Cancel"), reducing accidental confirmations.

## React Components

# Modal

## Overview

---

## Anatomy

---

Modal

ModalBody

ModalContent

ModalHeader

ModalTrigger

---

## Modal

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
backdropStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the modal backdrop. Useful if you want to override the backdrop z-index. |
| 

closeOnEscape

 | `boolean` | - | `true` | Whether to close the modal when the escape key is pressed. |
| 

closeOnInteractOutside

 | `boolean` | - | `true` | Whether to close the modal when the outside is clicked. |
| 

defaultOpen

 | `boolean` | - | `undefined` | The initial open state of the modal. Use when you don't need to control the open state of the modal. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

initialFocusedElement

 | `() => HTMLElement | null` | - | `undefined` | Element that receive the focus when the dialog is opened. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

onOpenChange

 | `(detail: ModalOpenChangeDetail) => void` | - | `undefined` | Callback fired when the modal open state changes. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the modal. |
| 

positionerStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |

## ModalBody

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## ModalContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
colorDeprecated

 | `MODAL_COLOR` | - | `MODAL_COLOR.information` | The color preset to use. DEPRECATED: Color is no longer used and will be removed in the next major version. |
| 

createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. |
| 

dismissible

 | `boolean` | - | `true` | Whether the remove button is displayed. |

## ModalHeader

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## ModalTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## Enums

---

### MODAL_COLOR

Deprecated

-   critical =`"critical"`
-   information =`"information"`
-   neutral =`"neutral"`
-   primary =`"primary"`
-   success =`"success"`
-   warning =`"warning"`

### MODAL_I18N

-   closeButton =`"modal.close.button"`

## Interfaces

---

### ModalOpenChangeDetail

-   `open: boolean`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-modal-border-radius | var(--ods-theme-overlay-border-radius) | 
 |
| --ods-modal-header-background-color-critical | var(--ods-color-critical-075) | 

 |
| --ods-modal-header-background-color-information | var(--ods-color-information-075) | 

 |
| --ods-modal-header-background-color-neutral | var(--ods-color-neutral-075) | 

 |
| --ods-modal-header-background-color-primary | var(--ods-color-primary-500) | 

 |
| --ods-modal-header-background-color-success | var(--ods-color-success-075) | 

 |
| --ods-modal-header-background-color-warning | var(--ods-color-warning-075) | 

 |
| --ods-modal-header-height | 32px | 

 |
| --ods-modal-header-padding-horizontal | calc(var(--ods-theme-padding-horizontal) * 2) | 

 |
| --ods-modal-header-padding-vertical | calc(var(--ods-theme-padding-vertical) * 2) | 

 |
| --ods-modal-margin-horizontal | calc(var(--ods-theme-padding-horizontal) * 2) | 

 |
| --ods-modal-margin-vertical | calc(var(--ods-theme-padding-vertical) * 2) | 

 |
| --ods-modal-mobile-max-height | 512px | 

 |
| --ods-modal-mobile-max-width | 512px | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger>
        Trigger Modal      </ModalTrigger>
      <ModalContent>
        <ModalHeader>My modal header</ModalHeader>
        <ModalBody>
          My modal content        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Controlled

```jsx
const [isOpen, setIsOpen] = useState(false);
  function onOpenChange({
    open  }: ModalOpenChangeDetail) {
    setIsOpen(open);
  }
  function openModal() {
    setIsOpen(true);
  }
  return <>
      <Button onClick={openModal}>
        Trigger Modal      </Button>
      <Modal onOpenChange={onOpenChange} open={isOpen}>
        <ModalContent>
          <ModalHeader>Controlled modal</ModalHeader>
          <ModalBody>
            Content          </ModalBody>
        </ModalContent>
      </Modal>
    </>;
}
```

### Non Dismissible

```jsx
<Modal>
  <ModalTrigger asChild>
    <Button>
      Trigger Modal    </Button>
  </ModalTrigger>
  <ModalContent dismissible={false}>
    <ModalHeader>Non dismissible</ModalHeader>
    <ModalBody>
      My modal content    </ModalBody>
  </ModalContent>
</Modal>
```

### Non Escapable

```jsx
<Modal closeOnEscape={false} closeOnInteractOutside={false}>
  <ModalTrigger asChild>
    <Button>
      Trigger Modal    </Button>
  </ModalTrigger>
  <ModalContent>
    <ModalHeader>Non escapable</ModalHeader>
    <ModalBody>
      My modal content    </ModalBody>
  </ModalContent>
</Modal>
```

### Overlay Elements

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Button, Icon, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger, Select, SelectContent, SelectControl, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal        </Button>
      </ModalTrigger>
      <ModalContent>
        <ModalHeader>Overlay elements</ModalHeader>
        <ModalBody style={{
        display: 'grid',
        columnGap: '8px',
        alignItems: 'center',
        gridTemplateColumns: '1fr max-content'
      }}>
          <Select items={[{
          label: 'Dog',
          value: 'dog'
        }, {
          label: 'Cat',
          value: 'cat'
        }, {
          label: 'Hamster',
          value: 'hamster'
        }, {
          label: 'Parrot',
          value: 'parrot'
        }, {
          label: 'Spider',
          value: 'spider'
        }, {
          label: 'Goldfish',
          value: 'goldfish'
        }]}>
            <SelectControl />
            <SelectContent createPortal={false} />
          </Select>
          <Tooltip>
            <TooltipTrigger asChild>
              <Icon name={ICON_NAME.circleQuestion} style={{
              fontSize: '24px'
            }} />
            </TooltipTrigger>
            <TooltipContent createPortal={false}>
              This is the tooltip content            </TooltipContent>
          </Tooltip>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

## Recipes

---

Status Modal

## React Components/Modal

## Subcomponents


### ModalBody




### ModalContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No | MODAL_COLOR.information | @deprecated The color preset to use. DEPRECATED: Color is no longer used and will be removed in the next major version. |
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. |
| `dismissible` | `` | No | true | Whether the remove button is displayed. |



### ModalHeader




### ModalTrigger



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `asChild` | `` | No |  | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |


## Examples


### Accessibility Actions

```tsx
{
  globals: {
    imports: `import { BUTTON_COLOR, BUTTON_VARIANT, MODAL_COLOR, Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal
        </Button>
      </ModalTrigger>

      <ModalContent color={MODAL_COLOR.critical}>
        <ModalHeader><span id="modal-title">Delete item</span></ModalHeader>
        <ModalBody>
          <p id="modal-content">
            Are you sure you want to delete this item?
          </p>

          <div style={{
          display: 'flex',
          gap: '8px',
          justifyContent: 'end'
        }}>
            <Button variant={BUTTON_VARIANT.ghost}>
              Cancel
            </Button>

            <Button color={BUTTON_COLOR.critical}>
              Delete
            </Button>
          </div>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Accessibility Aria Label

```tsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal
        </Button>
      </ModalTrigger>

      <ModalContent aria-describedby="modal-content" aria-label="Modal Content">
        <ModalHeader>Modal header</ModalHeader>
        <ModalBody id="modal-content">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Accessibility Aria Labelled By

```tsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal
        </Button>
      </ModalTrigger>

      <ModalContent aria-describedby="modal-content" aria-labelledby="modal-title">
        <ModalHeader><span id="modal-title">Delete item</span></ModalHeader>
        <ModalBody>
          <p id="modal-content">
            Are you sure you want to delete this item? This action cannot be undone.
          </p>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Accessibility Bad Practices Aria

```tsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal
        </Button>
      </ModalTrigger>

      <ModalContent>
        <ModalHeader><span id="modal-title">Delete item</span></ModalHeader>
        <ModalBody>
          <p id="modal-content">
            Are you sure you want to delete this item? This action cannot be undone.
          </p>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: 'var(--ods-theme-row-gap)',
    alignItems: 'start'
  }}>
      <div style={{
      position: 'relative',
      minWidth: '320px',
      minHeight: '140px'
    }}>
        <Modal backdropStyle={{
        position: 'absolute'
      }}
      // @ts-ignore force ark attribute to avoid inert on page
      modal={false} open positionerStyle={{
        position: 'absolute'
      }}>
          <ModalContent createPortal={false} style={{
          width: '280px',
          minWidth: 'auto',
          animation: 'none'
        }}>
            <ModalHeader>
              <Text as="span" preset={TEXT_PRESET.heading4}>
                Overview
              </Text>
            </ModalHeader>

            <ModalBody>
                Lorem ipsum dolor sit amet ...
            </ModalBody>
          </ModalContent>
        </Modal>
      </div>

      <Modal>
        <ModalTrigger asChild>
          <Button variant={BUTTON_VARIANT.outline}>
            Trigger Modal
          </Button>
        </ModalTrigger>
      </Modal>
    </div>
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [isOpen, setIsOpen] = useState(false);
    function onOpenChange({
      open
    }: ModalOpenChangeDetail) {
      setIsOpen(open);
    }
    function openModal() {
      setIsOpen(true);
    }
    return <>
        <Button onClick={openModal}>
          Trigger Modal
        </Button>

        <Modal onOpenChange={onOpenChange} open={isOpen}>
          <ModalContent>
            <ModalHeader>Controlled modal</ModalHeader>
            <ModalBody>
              Content
            </ModalBody>
          </ModalContent>
        </Modal>
      </>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger>
        Trigger Modal
      </ModalTrigger>
      <ModalContent>
        <ModalHeader>My modal header</ModalHeader>
        <ModalBody>
          My modal content
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Modal closeOnEscape={arg.closeOnEscape} closeOnInteractOutside={arg.closeOnInteractOutside}>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal
        </Button>
      </ModalTrigger>

      <ModalContent color={arg.color} dismissible={arg.dismissible}>
        <ModalHeader>Modal header</ModalHeader>
        <ModalBody>
          {arg.content}
        </ModalBody>
      </ModalContent>
    </Modal>,
  argTypes: orderControls({
    closeOnEscape: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    closeOnInteractOutside: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    content: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    dismissible: {
      table: {
        category: CONTROL_CATEGORY.general,
        defaultValue: {
          summary: true
        },
        type: {
          summary: 'boolean'
        }
      },
      control: {
        type: 'boolean'
      }
    }
  }),
  args: {
    content: 'My modal content'
  }
}
```

### Non Dismissible

```tsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal
        </Button>
      </ModalTrigger>

      <ModalContent dismissible={false}>
        <ModalHeader>Non dismissible</ModalHeader>
        <ModalBody>
          My modal content
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Non Escapable

```tsx
{
  globals: {
    imports: `import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => <Modal closeOnEscape={false} closeOnInteractOutside={false}>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal
        </Button>
      </ModalTrigger>

      <ModalContent>
        <ModalHeader>Non escapable</ModalHeader>
        <ModalBody>
          My modal content
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Overlay Elements

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Button, Icon, Modal, ModalBody, ModalContent, ModalHeader, ModalTrigger, Select, SelectContent, SelectControl, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button>
          Trigger Modal
        </Button>
      </ModalTrigger>

      <ModalContent>
        <ModalHeader>Overlay elements</ModalHeader>
        <ModalBody style={{
        display: 'grid',
        columnGap: '8px',
        alignItems: 'center',
        gridTemplateColumns: '1fr max-content'
      }}>
          <Select items={[{
          label: 'Dog',
          value: 'dog'
        }, {
          label: 'Cat',
          value: 'cat'
        }, {
          label: 'Hamster',
          value: 'hamster'
        }, {
          label: 'Parrot',
          value: 'parrot'
        }, {
          label: 'Spider',
          value: 'spider'
        }, {
          label: 'Goldfish',
          value: 'goldfish'
        }]}>
            <SelectControl />

            <SelectContent createPortal={false} />
          </Select>

          <Tooltip>
            <TooltipTrigger asChild>
              <Icon name={ICON_NAME.circleQuestion} style={{
              fontSize: '24px'
            }} />
            </TooltipTrigger>

            <TooltipContent createPortal={false}>
              This is the tooltip content
            </TooltipContent>
          </Tooltip>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Modal>
      <ModalTrigger asChild>
        <Button variant={BUTTON_VARIANT.outline}>
          Trigger Modal
        </Button>
      </ModalTrigger>

      <ModalContent>
        <ModalHeader>
          <Text as="span" preset={TEXT_PRESET.heading4}>
            Overview
          </Text>
        </ModalHeader>
        <ModalBody>
          <Text>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
          </Text>
        </ModalBody>
      </ModalContent>
    </Modal>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '12px'
  }}>
      <Modal>
        <ModalTrigger asChild>
          <Button>Default</Button>
        </ModalTrigger>
        <ModalContent createPortal={false}>
          <ModalHeader>Default</ModalHeader>
          <ModalBody>Default</ModalBody>
        </ModalContent>
      </Modal>

      <Modal>
        <ModalTrigger asChild>
          <Button>Non dismissible</Button>
        </ModalTrigger>
        <ModalContent createPortal={false} dismissible={false}>
          <ModalHeader>Non dismissible</ModalHeader>
          <ModalBody>Non dismissible</ModalBody>
        </ModalContent>
      </Modal>

      <Modal>
        <ModalTrigger asChild>
          <Button color={BUTTON_COLOR.critical}>Critical</Button>
        </ModalTrigger>
        <ModalContent color={MODAL_COLOR.critical} createPortal={false}>
          <ModalHeader>Critical</ModalHeader>
          <ModalBody>Critical</ModalBody>
        </ModalContent>
      </Modal>
    </div>
}
```

## React Components

# Pagination

_A **Pagination** component allows users to navigate through large sets of data by dividing the content into multiple pages._

102550100300

of 100 results

Go to page

## Overview

---

The **Pagination** component is used to divide content into discrete pages and provide navigation controls to move between them.

This component enhances usability by allowing users to browse through large sets of data without overwhelming the interface.

**Pagination** can include various controls like next/previous buttons and page numbers.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Pagination</td></tr><tr><th scope="row">Also known as</th><td>Page navigation</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=47-7743" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/pagination" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-pagination--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

A **Pagination** component is used in two situations :

-   To navigate among a  component
-   To browse in a set of items (products list, ...)

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Pagination when users need to navigate through large datasets in a structured and orderly way |
| - Ensure Pagination is used when the total number of pages is known or predictable |
| - Display a reasonable number of items per page (typically around 20–30) to balance readability and navigation |
| - Use Next/Previous buttons and page numbers to help users track their position within the dataset |

| ❌ Don't |
| --- |
| - Don't use Pagination when the total content fits into a single page |
| - Avoid Pagination if the number of pages is unpredictable or constantly changing. In those cases, consider infinite scrolling or "Load more" patterns instead |
| - Don't make Pagination the only method of navigation if users might need to filter or search within the dataset |

### Best Practices in Context

1.  **Pagination**
2.  **Total items per page select**
3.  **Total amount of items**
4.  **Previous/Next buttons**
5.  **Ellipsis**
6.  **Current page button**
7.  **Unselected page button**

## Placement

---

**Pagination** is presented to the user near the container it uses for pagination; it can be located just above or below it.

Usually, the **Pagination** is end-aligned horizontally, as its usage is not principal to the container.

## Behavior

---

### Amount of items per page

The number of items displayed in **Pagination** is depending on its referential.

However, there are few rules to be applied to display it correctly.

Number of items in the component can be chunked in packs of :

-   10
-   25
-   50
-   100
-   300

This value is conditioning the number of items that will be displayed per page.

By default, 10 items per page are displayed, but it can be set to match your need.

### Amount of pages

A maximum of 6 numbered page  can be visible at once, with a minimum of 1.

Arrow Buttons are always visible, no matter what the amount of pages.

Depending on the current page number and the amount of pages, here are the different displays of the whole component:

-   If amount of pages is less than 7, all numbered page  can be visible at once
-   If amount of pages is 7 or more :
-   If current page is the 4th one or less, the first 5 numbered  are present, then an ellipsis and the last numbered  corresponding of the amount of pages
-   If current page is the 4th to last or more, the first page  is displayed followed by an ellipsis and the last 5 numbered 
-   If current page is between the previous bounds are displayed, in order :
-   the first page ,
-   an ellipsis,
-   3 numbered , corresponding to : previous-to-current / current / next-to-current page,
-   an ellipsis,
-   the last numbered 

### Go to page

Allows users to directly select a page from an number input.

## Navigation

---

### Focus Management

When tabbing through the page, focus moves sequentially through all interactive elements in the **Pagination** component.

The current page Button is not interactive.

Disabled navigation Buttons (e.g., “Previous” on the first page) are skipped in the tab order.

### General Keyboard Shortcuts

Pressing Tab moves focus forward through all interactive elements in the **Pagination** (Select, Buttons).

Pressing Shift + Tab moves focus backward.

Pressing Enter or Space on a page button, "Previous", or "Next" triggers the corresponding page change.

The "items per page" Select uses the same keyboard shortcuts as the standard Select component.

## Accessibility

---

**Pagination** component should be properly identified, and correct labels must be implemented to ensure it is accessible to assistive technologies.

### Identifying the Pagination with aria-label

**Pagination** is a form of navigation, but it serves a specific purpose distinct from primary navigation menus. To ensure it is correctly recognized, an [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) should be added to explicitly identify it.

```jsx
<Pagination
  aria-label="Pagination"
  totalItems={5000}
>
  <PaginationPages />
</Pagination>
```

Screen readers will announce the pagination element correctly.

## React Components

# Pagination

## Overview

---

## Anatomy

---

Pagination

PaginationPageSelector

PaginationPageSizeSelector

PaginationPages

---

## Pagination

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [nav attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/nav#attributes) . |
| 
defaultPage

 | `number` | - | `undefined` | The initial active page. Use when you don't need to control the active page of the pagination. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

labelTooltipNext

 | `string` | - | `undefined` | The tooltip label on the "next page" button. |
| 

labelTooltipPrev

 | `string` | - | `undefined` | The tooltip label on the "previous page" button. |
| 

onPageChange

 | `(detail: PaginationPageChangeDetail) => void` | - | `undefined` | Callback fired when the active page changes. |
| 

onPageSizeChange

 | `(detail: PaginationPageSizeChangeDetail) => void` | - | `undefined` | Callback fired when the page size changes. |
| 

page

 | `number` | - | `undefined` | The controlled active page |
| 

pageSize

 | `number` | - | `undefined` | The number of items per page. |
| 

renderTotalItemsLabelDeprecated

 | `(params: { totalItems: number }) => string | number` | - | `'of ${totalItems} results'` | Format the label displayed near the per-page selector. DEPRECATED: prefer the use of the sub component PaginationPageSizeSelector |
| 

siblingCount

 | `number` | - | `undefined` | The number of pages to show beside active page. |
| 

totalItems

 | `number` |  | `undefined` | The total number of items. |
| 

withPageSizeSelectorDeprecated

 | `boolean` | - | `undefined` | Whether the per-page selector is displayed. DEPRECATED: prefer the use of the sub component PaginationPageSizeSelector |

## PaginationPageSelector

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [form attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#attributes) . |
| 
label

 | `ReactNode` | - | `'Go to page'` | The label displayed near the go-to-page input. |
| 

submitLabel

 | `ReactNode` | - | `'Go'` | The label displayed in the go-to-page submit button. |

## PaginationPageSizeSelector

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
label

 | `ReactNode` | - | `'of ${totalItems} results'` | The label displayed near the per-page selector. |

## PaginationPages

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## Enums

---

### PAGINATION_PER_PAGE

-   option_10 =`"10"`
-   option_100 =`"100"`
-   option_25 =`"25"`
-   option_300 =`"300"`
-   option_50 =`"50"`

## Interfaces

---

### PaginationPageChangeDetail

-   `page: number`
-   `pageSize: number`

### PaginationPageSizeChangeDetail

-   `pageSize: number`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-pagination-page-selector-input-width | 45px | 
 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination totalItems={5000}>
      <PaginationPages />
    </Pagination>
}
```

### Controlled

### Disabled

```jsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination disabled totalItems={500}>
      <PaginationPages />
    </Pagination>
}
```

### Items per page

```jsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination pageSize={25} totalItems={500}>
      <PaginationPages />
    </Pagination>
}
```

### Sibling count

```jsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination defaultPage={5} siblingCount={2} totalItems={500}>
      <PaginationPages />
    </Pagination>
}
```

### With tooltip labels

```jsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination labelTooltipPrev="Go to previous page" labelTooltipNext="Go to next page" totalItems={500}>
      <PaginationPages />
    </Pagination>
}
```

### With page size selection

```jsx
{
  globals: {
    imports: `import { Pagination, PaginationPageSizeSelector, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination totalItems={500}>
      <PaginationPageSizeSelector />
      <PaginationPages />
    </Pagination>
}
```

## Recipes

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

Data Grid With Query Filter

| 
Instance ID

 | 

Location

 | 

Model

 | 

Image

 | 

Backup Logic

 | 

Running since

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

## React Components/Pagination

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultPage` | `` | No |  | The initial active page. Use when you don't need to control the active page of the pagination. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `labelTooltipNext` | `` | No |  | The tooltip label on the "next page" button. |
| `labelTooltipPrev` | `` | No |  | The tooltip label on the "previous page" button. |
| `onPageChange` | `` | No |  | Callback fired when the active page changes. |
| `onPageSizeChange` | `` | No |  | Callback fired when the page size changes. |
| `page` | `` | No |  | The controlled active page |
| `pageSize` | `` | No |  | The number of items per page. |
| `renderTotalItemsLabel` | `` | No |  | @deprecated @default-value='of ${totalItems} results' Format the label displayed near the per-page selector. DEPRECATED: prefer the use of the sub component PaginationPageSizeSelector |
| `siblingCount` | `` | No |  | The number of pages to show beside active page. |
| `totalItems` | `` | Yes |  | The total number of items. |
| `withPageSizeSelector` | `` | No |  | @deprecated Whether the per-page selector is displayed. DEPRECATED: prefer the use of the sub component PaginationPageSizeSelector |


## Subcomponents


### PaginationPageSelector



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `label` | `` | No | 'Go to page' | The label displayed near the go-to-page input. |
| `submitLabel` | `` | No | 'Go' | The label displayed in the go-to-page submit button. |



### PaginationPageSizeSelector



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `label` | `` | No |  | @default-value='of ${totalItems} results' The label displayed near the per-page selector. |



### PaginationPages



## Examples


### Accessibility Label

```tsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination aria-label="Pagination" totalItems={5000}>
      <PaginationPages />
    </Pagination>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Pagination totalItems={100}>
      <PaginationPageSizeSelector />
      <PaginationPages />
      <PaginationPageSelector />
    </Pagination>
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [page, setPage] = useState(1);
    function handlePageChange({
      page
    }: PaginationPageChangeDetail) {
      setPage(page);
    }
    return <Pagination onPageChange={handlePageChange} page={page} totalItems={500}>
        <PaginationPages />
      </Pagination>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination totalItems={5000}>
      <PaginationPages />
    </Pagination>
}
```

### Demo

```tsx
{
  render: ({
    totalItems,
    withPageSelector,
    withPageSizeSelector,
    ...arg
  }: DemoArg) => <Pagination totalItems={totalItems ?? 5000} {...arg}>
      {withPageSizeSelector && <PaginationPageSizeSelector />}

      <PaginationPages />

      {withPageSelector && <PaginationPageSelector />}
    </Pagination>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    labelTooltipNext: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    labelTooltipPrev: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    pageSize: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    page: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    siblingCount: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    totalItems: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    withPageSelector: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    withPageSizeSelector: {
      table: {
        category: CONTROL_CATEGORY.general
      }
    }
  }),
  args: {
    totalItems: 5000
  }
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination disabled totalItems={500}>
      <PaginationPages />
    </Pagination>
}
```

### Items Per Page

```tsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination pageSize={25} totalItems={500}>
      <PaginationPages />
    </Pagination>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Pagination totalItems={100}>
      <PaginationPageSizeSelector />
      <PaginationPages />
      <PaginationPageSelector />
    </Pagination>
}
```

### Page Size Selection

```tsx
{
  globals: {
    imports: `import { Pagination, PaginationPageSizeSelector, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination totalItems={500}>
      <PaginationPageSizeSelector />

      <PaginationPages />
    </Pagination>
}
```

### Sibling Count

```tsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination defaultPage={5} siblingCount={2} totalItems={500}>
      <PaginationPages />
    </Pagination>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    alignItems: 'flex-start'
  }}>
      <Pagination totalItems={100}>
        <PaginationPages />
      </Pagination>
      <Pagination totalItems={500} pageSize={25}>
        <PaginationPages />
      </Pagination>
      <Pagination totalItems={500} disabled>
        <PaginationPages />
      </Pagination>
      <Pagination totalItems={100}>
        <PaginationPageSizeSelector />
        <PaginationPages />
        <PaginationPageSelector />
      </Pagination>
    </div>
}
```

### With Tooltip Labels

```tsx
{
  globals: {
    imports: `import { Pagination, PaginationPages } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Pagination labelTooltipPrev="Go to previous page" labelTooltipNext="Go to next page" totalItems={500}>
      <PaginationPages />
    </Pagination>
}
```

## React Components

# Password

_**Password** component is an  field for entering a password that can be hidden or not._

## Overview

---

The **Password** component is an Input field of which the content is replaced with middle dot symbol symbols "·" by default and the masking can be toggled using a show/hide action.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Password</td></tr><tr><th scope="row">Also known as</th><td>Password field, Password input</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=48-4526" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/password" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-password--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

A **Password** is mainly used to let users enter a password or secret information.

Entered password can be masked or visible as plain text when users need to check what they have entered.

It can also be used for displaying tokens.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the Password component for sensitive inputs, such as login credentials, tokens, or API keys |
| - Use the read-only mode for displaying generated tokens or credentials that users may need to copy, without editing |
| - Pair Password fields with clear labels and helper texts for expected format or complexity requirements |

| ❌ Don't |
| --- |
| - Don't use the Password component when users need to see what they're typing by default, use a standard Input instead |
| - Don't hide the label. Users should always know what the Password field is for |
| - Don't use Passwords for non-sensitive or public information; this could cause unnecessary friction |
| - Don't rely only on placeholders to communicate password requirements |
| - Don't remove the toggle visibility option unless there is a strong security or UX reason to do so |

### Best Practices in Context

1.  **Password**
2.  **Placeholder or input text**
3.  **Show/Hide button**
4.  **Clearable button** - optional

## Placement

---

**Password** should be vertically aligned with other form components on a same page.

## Behavior

---

**Password** can be used in read-only mode, especially for displaying sensitive tokens.

Users can toggle the password masking (show/hide) by clicking an action button. The selected visibility state remains until the user clicks again.

**Password** supports a clear action to quickly reset its content.

A loading state can be displayed inside the field when needed, for example during validation or processing.

## Navigation

---

### Focus Management

When tabbing through the page, the Password field receives focus as part of the natural tab order.

If present, the clear button is focusable immediately after the Password field.

If present, the toggle mask button (show/hide password) is focusable after the clear button.

If the Password field is read-only, it can still receive focus but cannot be edited.

### General Keyboard Shortcuts

Pressing Tab moves focus forward.

Pressing Shift + Tab moves focus backward to the previous interactive element.

Pressing any character key while the Password field is focused enters text into the field (unless it is read-only).

Pressing Backspace deletes the last character before the cursor position.

Pressing Enter while the clear button is focused clears the Password content.

Pressing Enter while the toggle mask button is focused toggles between showing and hiding the field content.

## Accessibility

---

To ensure proper accessibility, the **Password** component must be correctly labeled and provide meaningful context when interactive elements (such as icon buttons) are used.

### Always provide an explicit label

Every **Password** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Password:

```jsx
<FormField>
  <FormFieldLabel>
    Password:  </FormFieldLabel>
  <Password />
</FormField>
```

Screen readers will announce the label, the field and its content.

### Override action context

To provide more context on the interactive elements, you can provide your own custom translations to the component.

Password:

```jsx
<FormField>
  <FormFieldLabel>
    Password:  </FormFieldLabel>
  <Password i18n={{
  [INPUT_I18N.maskButtonHide]: 'Hide the password',
  [INPUT_I18N.maskButtonShow]: 'Show the password'
}} />
</FormField>
```

Screen readers will announce the label, the field, its content and custom label of focused action.

## React Components

# Password

## Overview

---

## Anatomy

---

Password

---

## Password

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [input attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#attributes) . |
| 
clearable

 | `boolean` | - | `undefined` | Whether the clear button is displayed. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override (see Input i18n keys). |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

loading

 | `boolean` | - | `undefined` | Whether the component is in loading state. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

maskInitialState

 | `INPUT_MASK_STATE` | - | `undefined` | The masked display initial state. |
| 

onClear

 | `() => void` | - | `undefined` | Callback fired when the input value is cleared. |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password />
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password disabled />
}
```

### Readonly

```jsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password defaultValue="Readonly" readOnly />
}
```

### Clearable

```jsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password clearable defaultValue="Clearable" />
}
```

### Loading

```jsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password loading />
}
```

### Form field

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Password:      </FormFieldLabel>
      <Password />
    </FormField>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Password

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `clearable` | `` | No |  | Whether the clear button is displayed. |
| `i18n` | `` | No |  | Internal translations override (see Input i18n keys). |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `loading` | `` | No |  | Whether the component is in loading state. |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `maskInitialState` | `` | No |  | The masked display initial state. |
| `onClear` | `` | No |  | Callback fired when the input value is cleared. |


## Examples


### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Password:
      </FormFieldLabel>

      <Password />
    </FormField>
}
```

### Accessibility I 18 N

```tsx
{
  globals: {
    imports: `import { INPUT_I18N, FormField, FormFieldLabel, Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => <FormField>
      <FormFieldLabel>
        Password:
      </FormFieldLabel>

      <Password i18n={{
      [INPUT_I18N.maskButtonHide]: 'Hide the password',
      [INPUT_I18N.maskButtonShow]: 'Show the password'
    }} />
    </FormField>
}
```

### Accessibility Label

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Password:
      </FormFieldLabel>

      <Password />
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Password />
}
```

### Clearable

```tsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password clearable defaultValue="Clearable" />
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    clearable: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    loading: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    placeholder: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password disabled />
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Password:
      </FormFieldLabel>

      <Password />
    </FormField>
}
```

### Loading

```tsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password loading />
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Password />
}
```

### Read Only

```tsx
{
  globals: {
    imports: `import { Password } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Password defaultValue="Readonly" readOnly />
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    alignItems: 'flex-start'
  }}>
      <Password placeholder="Default" />
      <Password clearable defaultValue="Clearable" />
      <Password loading placeholder="Loading" />
      <Password disabled placeholder="Disabled" />
      <Password invalid placeholder="Invalid" />
      <Password readOnly defaultValue="Read only" />
    </div>
}
```

## React Components

# Phone Number

_**Phone Number** component is a combo of a selection of country phone indicator and an  field for entering a phone number_

AfghanistanÅland IslandsAlbaniaAlgeriaAmerican SamoaAndorraAngolaAnguillaAntigua & BarbudaArgentinaArmeniaArubaAustraliaAustriaAzerbaijanBahamasBahrainBangladeshBarbadosBelarusBelgiumBelizeBeninBermudaBhutanBoliviaBosnia & HerzegovinaBotswanaBrazilBritish Indian Ocean TerritoryBritish Virgin IslandsBruneiBulgariaBurkina FasoBurundiCambodiaCameroonCanadaCape VerdeCaribbean NetherlandsCayman IslandsCentral African RepublicChadChileChinaChristmas IslandCocos (Keeling) IslandsColombiaComorosCongo - BrazzavilleCongo - KinshasaCook IslandsCosta RicaCôte d’IvoireCroatiaCubaCuraçaoCyprusCzechiaDenmarkDjiboutiDominicaDominican RepublicEcuadorEgyptEl SalvadorEquatorial GuineaEritreaEstoniaEswatiniEthiopiaFalkland Islands (Islas Malvinas)Faroe IslandsFijiFinlandFranceFrench GuianaFrench PolynesiaGabonGambiaGeorgiaGermanyGhanaGibraltarGreeceGreenlandGrenadaGuadeloupeGuamGuatemalaGuernseyGuineaGuinea-BissauGuyanaHaitiHondurasHong KongHungaryIcelandIndiaIndonesiaIranIraqIrelandIsle of ManIsraelItalyJamaicaJapanJerseyJordanKazakhstanKenyaKiribatiKuwaitKyrgyzstanLaosLatviaLebanonLesothoLiberiaLibyaLiechtensteinLithuaniaLuxembourgMacaoMadagascarMalawiMalaysiaMaldivesMaliMaltaMarshall IslandsMartiniqueMauritaniaMauritiusMayotteMexicoMicronesiaMoldovaMonacoMongoliaMontenegroMontserratMoroccoMozambiqueMyanmar (Burma)NamibiaNauruNepalNetherlandsNew CaledoniaNew ZealandNicaraguaNigerNigeriaNiueNorfolk IslandNorth KoreaNorth MacedoniaNorthern Mariana IslandsNorwayOmanPakistanPalauPalestinePanamaPapua New GuineaParaguayPeruPhilippinesPolandPortugalPuerto RicoQatarRéunionRomaniaRussiaRwandaSamoaSan MarinoSão Tomé & PríncipeSaudi ArabiaSenegalSerbiaSeychellesSierra LeoneSingaporeSint MaartenSlovakiaSloveniaSolomon IslandsSomaliaSouth AfricaSouth KoreaSouth SudanSpainSri LankaSt. BarthélemySt. HelenaSt. Kitts & NevisSt. LuciaSt. MartinSt. Pierre & MiquelonSt. Vincent & GrenadinesSudanSurinameSvalbard & Jan MayenSwedenSwitzerlandSyriaTaiwanTajikistanTanzaniaThailandTimor-LesteTogoTokelauTongaTrinidad & TobagoTunisiaTürkiyeTurkmenistanTurks & Caicos IslandsTuvaluU.S. Virgin IslandsUgandaUkraineUnited Arab EmiratesUnited KingdomUnited StatesUruguayUzbekistanVanuatuVatican CityVenezuelaVietnamWallis & FutunaWestern SaharaYemenZambiaZimbabwe

## Overview

---

**Phone Number** component is used to let users enter their phone number in the correct format for the selected country.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Phone Number</td></tr><tr><th scope="row">Also known as</th><td>Phone Number Field</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=48-6130" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/phone-number" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-phone-number--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

**Phone Number** component should be used when there is a need to collect the user's phone number, in a form for instance:

-   An user profile
-   A contact/appointment form
-   For telecom configuration

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the country indicator Select when your application supports international phone numbers |
| - Display helper text or error messaging to guide users if the input format is incorrect (e.g., expected digit count or formatting rules) |
| - If phone numbers are only accepted in the user's locale, use the component without the country code Select |

| ❌ Don't |
| --- |
| - Don't enforce a rigid format (e.g., requiring users to enter parentheses or dashes), prioritize forgiving input |
| - Don't make the field required without context. Explain clearly why the phone number is needed (e.g., for account recovery or verification) |
| - Don't assume all users have mobile numbers. Allow landline formats where applicable |
| - Don't display country codes or formats that aren't relevant to your supported markets |

### Best Practices in Context

1.  **Phone Number**
2.  **Country selector** - optional
3.  **Input field**
4.  **Clearable button** - optional

## Placement

---

**Phone Number** is a group of  and , and should act as their specific placements.

## Behavior

---

When the user selects a country, it determines the format used to validate their phone number. If selected country has been modified, expected format and placeholder will be updated.

If the field content is in error state (i.e. missing or wrong characters), the whole **Phone Number** component becomes in error state.

The country selector allows users to navigate to the desired country option by typing letters while focused on the country selector.

The search is based on the start of the word and functions one letter at a time.

For example:

-   Typing "f" focus the first country that starts with "f".
-   Typing "r" immediately after focus the first country that starts with "r".

### Locale

The locale (i.e. country list translation in ) is first set to the value provided as a property.

If the given property is not defined or recognized, the component attempts to use the browser's locale settings.

If the browser's locale is also not recognized, the component defaults to English (EN).

### ISO code

The ISO code is initially set to the value provided as a property.

If the given property is not defined or recognized, the component attempts to determine the ISO code based on the browser's locale.

If the browser's locale is not recognized, the component defaults to the first ISO code in the predefined country list.

## Navigation

---

### Focus Management

When the **Phone Number** component is focused, focus is first set to the country selector dropdown, if present, or directly to the Input field.

Each subcomponent (Select and Input) can be focused independently using keyboard navigation.

If the country selector is disabled or not rendered, focus starts directly on the Input field.

### General Keyboard Shortcuts

Pressing Tab moves focus forward:

-   First to the country selector (if present and enabled)
-   Then to the Phone Number Input field

Pressing Shift + Tab moves focus backward through these elements.

While focused on the country selector, keyboard shortcuts are similar to the Select component:

-   Pressing Space or Arrow Down opens the dropdown
-   Pressing Arrow keys navigates through the list of countries
-   Pressing Home/fn+ Arrow Up or End/fn + Arrow Down jumps to the first or last option
-   Typing letters focuses the first country whose name starts with the typed character
-   Pressing Enter or Tab selects the focused country and closes the dropdown
-   Pressing Escape closes the dropdown without selection

While focused on the Input field:

-   Typing numeric characters enters the phone number
-   Pressing Backspace deletes the last character
-   Pressing Arrow Left or Arrow Right moves the cursor within the input

## Accessibility

---

To ensure proper accessibility, the **Phone Number** component must be correctly labeled and provide meaningful context when interactive elements (such as icon buttons) are used.

### Always provide an explicit label

Every **Phone Number** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Phone number:

AfghanistanÅland IslandsAlbaniaAlgeriaAmerican SamoaAndorraAngolaAnguillaAntigua & BarbudaArgentinaArmeniaArubaAustraliaAustriaAzerbaijanBahamasBahrainBangladeshBarbadosBelarusBelgiumBelizeBeninBermudaBhutanBoliviaBosnia & HerzegovinaBotswanaBrazilBritish Indian Ocean TerritoryBritish Virgin IslandsBruneiBulgariaBurkina FasoBurundiCambodiaCameroonCanadaCape VerdeCaribbean NetherlandsCayman IslandsCentral African RepublicChadChileChinaChristmas IslandCocos (Keeling) IslandsColombiaComorosCongo - BrazzavilleCongo - KinshasaCook IslandsCosta RicaCôte d’IvoireCroatiaCubaCuraçaoCyprusCzechiaDenmarkDjiboutiDominicaDominican RepublicEcuadorEgyptEl SalvadorEquatorial GuineaEritreaEstoniaEswatiniEthiopiaFalkland Islands (Islas Malvinas)Faroe IslandsFijiFinlandFranceFrench GuianaFrench PolynesiaGabonGambiaGeorgiaGermanyGhanaGibraltarGreeceGreenlandGrenadaGuadeloupeGuamGuatemalaGuernseyGuineaGuinea-BissauGuyanaHaitiHondurasHong KongHungaryIcelandIndiaIndonesiaIranIraqIrelandIsle of ManIsraelItalyJamaicaJapanJerseyJordanKazakhstanKenyaKiribatiKuwaitKyrgyzstanLaosLatviaLebanonLesothoLiberiaLibyaLiechtensteinLithuaniaLuxembourgMacaoMadagascarMalawiMalaysiaMaldivesMaliMaltaMarshall IslandsMartiniqueMauritaniaMauritiusMayotteMexicoMicronesiaMoldovaMonacoMongoliaMontenegroMontserratMoroccoMozambiqueMyanmar (Burma)NamibiaNauruNepalNetherlandsNew CaledoniaNew ZealandNicaraguaNigerNigeriaNiueNorfolk IslandNorth KoreaNorth MacedoniaNorthern Mariana IslandsNorwayOmanPakistanPalauPalestinePanamaPapua New GuineaParaguayPeruPhilippinesPolandPortugalPuerto RicoQatarRéunionRomaniaRussiaRwandaSamoaSan MarinoSão Tomé & PríncipeSaudi ArabiaSenegalSerbiaSeychellesSierra LeoneSingaporeSint MaartenSlovakiaSloveniaSolomon IslandsSomaliaSouth AfricaSouth KoreaSouth SudanSpainSri LankaSt. BarthélemySt. HelenaSt. Kitts & NevisSt. LuciaSt. MartinSt. Pierre & MiquelonSt. Vincent & GrenadinesSudanSurinameSvalbard & Jan MayenSwedenSwitzerlandSyriaTaiwanTajikistanTanzaniaThailandTimor-LesteTogoTokelauTongaTrinidad & TobagoTunisiaTürkiyeTurkmenistanTurks & Caicos IslandsTuvaluU.S. Virgin IslandsUgandaUkraineUnited Arab EmiratesUnited KingdomUnited StatesUruguayUzbekistanVanuatuVatican CityVenezuelaVietnamWallis & FutunaWestern SaharaYemenZambiaZimbabwe

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Phone number:      </FormFieldLabel>
      <PhoneNumber>
        <PhoneNumberCountryList />
        <PhoneNumberControl />
      </PhoneNumber>
    </FormField>
}
```

Screen readers will announce the label, the field and its content.

### Override action context

To provide more context on the interactive elements, you can provide your own custom translations to the component.

```jsx
<FormField>
  <FormFieldLabel>
    Phone number:  </FormFieldLabel>
  <PhoneNumber country="fr" defaultValue="06 01 02 03 04" i18n={{
  [INPUT_I18N.clearButton]: 'Clear phone number'
}}>
    <PhoneNumberCountryList />
    <PhoneNumberControl clearable />
  </PhoneNumber>
</FormField>
```

Screen readers will announce the label, the field, its content and custom label of focused action.

## React Components

# Phone Number

## Overview

---

## Anatomy

---

PhoneNumber

PhoneNumberControl

PhoneNumberCountryList

---

## PhoneNumber

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
countries

 | `PhoneNumberCountryIsoCode[] | PhoneNumberCountriesPreset` | - | `undefined` | A specific or preset list of country to display in the selector. |
| 

country

 | `PHONE_NUMBER_COUNTRY_ISO_CODE` | - | `undefined` | The controlled selected country. |
| 

defaultValue

 | `string` | - | `undefined` | The initial phone number value. Use when you don't need to control the value of the phone number. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

id

 | `string` | - | `undefined` | The field id. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

locale

 | `string` | - | `undefined` | The locale used for the translation of the country list and the internal elements. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onCountryChange

 | `(detail: PhoneNumberCountryChangeDetail) => void` | - | `undefined` | Callback fired when the country changes. |
| 

onValueChange

 | `(detail: PhoneNumberValueChangeDetail) => void` | - | `undefined` | Callback fired when the value changes. |
| 

pattern

 | `string` | - | `undefined` | The phone number input expected pattern. |
| 

readOnly

 | `boolean` | - | `undefined` | Whether the component is readonly. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. |
| 

value

 | `string` | - | `undefined` | The controlled phone number value. |

## PhoneNumberControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [input attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#attributes) . |
| 
clearable

 | `boolean` | - | `undefined` | Whether the clear button is displayed. |
| 

loading

 | `boolean` | - | `undefined` | Whether the component is in loading state. |

## PhoneNumberCountryList

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## Enums

---

### PHONE_NUMBER_COUNTRIES_PRESET

-   all =`"all"`

### PHONE_NUMBER_COUNTRY_ISO_CODE

-   ad =`"ad"`
-   ae =`"ae"`
-   af =`"af"`
-   ag =`"ag"`
-   ai =`"ai"`
-   al =`"al"`
-   am =`"am"`
-   ao =`"ao"`
-   ar =`"ar"`
-   as =`"as"`
-   at =`"at"`
-   au =`"au"`
-   aw =`"aw"`
-   ax =`"ax"`
-   az =`"az"`
-   ba =`"ba"`
-   bb =`"bb"`
-   bd =`"bd"`
-   be =`"be"`
-   bf =`"bf"`
-   bg =`"bg"`
-   bh =`"bh"`
-   bi =`"bi"`
-   bj =`"bj"`
-   bl =`"bl"`
-   bm =`"bm"`
-   bn =`"bn"`
-   bo =`"bo"`
-   bq =`"bq"`
-   br =`"br"`
-   bs =`"bs"`
-   bt =`"bt"`
-   bw =`"bw"`
-   by =`"by"`
-   bz =`"bz"`
-   ca =`"ca"`
-   cc =`"cc"`
-   cd =`"cd"`
-   cf =`"cf"`
-   cg =`"cg"`
-   ch =`"ch"`
-   ci =`"ci"`
-   ck =`"ck"`
-   cl =`"cl"`
-   cm =`"cm"`
-   cn =`"cn"`
-   co =`"co"`
-   cr =`"cr"`
-   cu =`"cu"`
-   cv =`"cv"`
-   cw =`"cw"`
-   cx =`"cx"`
-   cy =`"cy"`
-   cz =`"cz"`
-   de =`"de"`
-   dj =`"dj"`
-   dk =`"dk"`
-   dm =`"dm"`
-   do =`"do"`
-   dz =`"dz"`
-   ec =`"ec"`
-   ee =`"ee"`
-   eg =`"eg"`
-   eh =`"eh"`
-   er =`"er"`
-   es =`"es"`
-   et =`"et"`
-   fi =`"fi"`
-   fj =`"fj"`
-   fk =`"fk"`
-   fm =`"fm"`
-   fo =`"fo"`
-   fr =`"fr"`
-   ga =`"ga"`
-   gb =`"gb"`
-   gd =`"gd"`
-   ge =`"ge"`
-   gf =`"gf"`
-   gg =`"gg"`
-   gh =`"gh"`
-   gi =`"gi"`
-   gl =`"gl"`
-   gm =`"gm"`
-   gn =`"gn"`
-   gp =`"gp"`
-   gq =`"gq"`
-   gr =`"gr"`
-   gt =`"gt"`
-   gu =`"gu"`
-   gw =`"gw"`
-   gy =`"gy"`
-   hk =`"hk"`
-   hn =`"hn"`
-   hr =`"hr"`
-   ht =`"ht"`
-   hu =`"hu"`
-   id =`"id"`
-   ie =`"ie"`
-   il =`"il"`
-   im =`"im"`
-   in =`"in"`
-   io =`"io"`
-   iq =`"iq"`
-   ir =`"ir"`
-   is =`"is"`
-   it =`"it"`
-   je =`"je"`
-   jm =`"jm"`
-   jo =`"jo"`
-   jp =`"jp"`
-   ke =`"ke"`
-   kg =`"kg"`
-   kh =`"kh"`
-   ki =`"ki"`
-   km =`"km"`
-   kn =`"kn"`
-   kp =`"kp"`
-   kr =`"kr"`
-   kw =`"kw"`
-   ky =`"ky"`
-   kz =`"kz"`
-   la =`"la"`
-   lb =`"lb"`
-   lc =`"lc"`
-   li =`"li"`
-   lk =`"lk"`
-   lr =`"lr"`
-   ls =`"ls"`
-   lt =`"lt"`
-   lu =`"lu"`
-   lv =`"lv"`
-   ly =`"ly"`
-   ma =`"ma"`
-   mc =`"mc"`
-   md =`"md"`
-   me =`"me"`
-   mf =`"mf"`
-   mg =`"mg"`
-   mh =`"mh"`
-   mk =`"mk"`
-   ml =`"ml"`
-   mm =`"mm"`
-   mn =`"mn"`
-   mo =`"mo"`
-   mp =`"mp"`
-   mq =`"mq"`
-   mr =`"mr"`
-   ms =`"ms"`
-   mt =`"mt"`
-   mu =`"mu"`
-   mv =`"mv"`
-   mw =`"mw"`
-   mx =`"mx"`
-   my =`"my"`
-   mz =`"mz"`
-   na =`"na"`
-   nc =`"nc"`
-   ne =`"ne"`
-   nf =`"nf"`
-   ng =`"ng"`
-   ni =`"ni"`
-   nl =`"nl"`
-   no =`"no"`
-   np =`"np"`
-   nr =`"nr"`
-   nu =`"nu"`
-   nz =`"nz"`
-   om =`"om"`
-   pa =`"pa"`
-   pe =`"pe"`
-   pf =`"pf"`
-   pg =`"pg"`
-   ph =`"ph"`
-   pk =`"pk"`
-   pl =`"pl"`
-   pm =`"pm"`
-   pr =`"pr"`
-   ps =`"ps"`
-   pt =`"pt"`
-   pw =`"pw"`
-   py =`"py"`
-   qa =`"qa"`
-   re =`"re"`
-   ro =`"ro"`
-   rs =`"rs"`
-   ru =`"ru"`
-   rw =`"rw"`
-   sa =`"sa"`
-   sb =`"sb"`
-   sc =`"sc"`
-   sd =`"sd"`
-   se =`"se"`
-   sg =`"sg"`
-   sh =`"sh"`
-   si =`"si"`
-   sj =`"sj"`
-   sk =`"sk"`
-   sl =`"sl"`
-   sm =`"sm"`
-   sn =`"sn"`
-   so =`"so"`
-   sr =`"sr"`
-   ss =`"ss"`
-   st =`"st"`
-   sv =`"sv"`
-   sx =`"sx"`
-   sy =`"sy"`
-   sz =`"sz"`
-   tc =`"tc"`
-   td =`"td"`
-   tg =`"tg"`
-   th =`"th"`
-   tj =`"tj"`
-   tk =`"tk"`
-   tl =`"tl"`
-   tm =`"tm"`
-   tn =`"tn"`
-   to =`"to"`
-   tr =`"tr"`
-   tt =`"tt"`
-   tv =`"tv"`
-   tw =`"tw"`
-   tz =`"tz"`
-   ua =`"ua"`
-   ug =`"ug"`
-   us =`"us"`
-   uy =`"uy"`
-   uz =`"uz"`
-   va =`"va"`
-   vc =`"vc"`
-   ve =`"ve"`
-   vg =`"vg"`
-   vi =`"vi"`
-   vn =`"vn"`
-   vu =`"vu"`
-   wf =`"wf"`
-   ws =`"ws"`
-   ye =`"ye"`
-   yt =`"yt"`
-   za =`"za"`
-   zm =`"zm"`
-   zw =`"zw"`

### PHONE_NUMBER_I18N

-   countrySelect =`"phoneNumber.country.select"`

### PHONE_NUMBER_PARSING_ERROR

-   invalidCountry =`"invalid-country"`
-   invalidLength =`"invalid-length"`
-   notANumber =`"not-a-number"`
-   tooLong =`"too-long"`
-   tooShort =`"too-short"`
-   unknownError =`"unknown-error"`

## Interfaces

---

### PhoneNumberCountryChangeDetail

-   `isNumberValid: boolean`
-   `value: PHONE_NUMBER_COUNTRY_ISO_CODE`

### PhoneNumberValueChangeDetail

-   `country?: PHONE_NUMBER_COUNTRY_ISO_CODE`
-   `formattedValue?: string`
-   `isNumberValid: boolean`
-   `parsingError?: string`
-   `value: string`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <PhoneNumber>
      <PhoneNumberControl />
    </PhoneNumber>
}
```

### Disabled

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <PhoneNumber disabled>
        <PhoneNumberControl />
      </PhoneNumber>
      <PhoneNumber disabled>
        <PhoneNumberCountryList />
        <PhoneNumberControl />
      </PhoneNumber>
    </>
}
```

### Readonly

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <PhoneNumber readOnly>
        <PhoneNumberControl />
      </PhoneNumber>
      <PhoneNumber readOnly>
        <PhoneNumberCountryList />
        <PhoneNumberControl />
      </PhoneNumber>
    </>
}
```

### Clearable

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <PhoneNumber>
        <PhoneNumberControl clearable />
      </PhoneNumber>
      <PhoneNumber>
        <PhoneNumberCountryList />
        <PhoneNumberControl clearable />
      </PhoneNumber>
    </>
}
```

### Loading

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <PhoneNumber>
        <PhoneNumberControl loading />
      </PhoneNumber>
      <PhoneNumber>
        <PhoneNumberCountryList />
        <PhoneNumberControl loading />
      </PhoneNumber>
    </>
}
```

### Locale

The locale manages the translation of the country list.

For further explanation about locale strategy, see .

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>Locale "fr"</span>
      <PhoneNumber locale="fr">
        <PhoneNumberCountryList />
        <PhoneNumberControl />
      </PhoneNumber>
      <span>Locale "de"</span>
      <PhoneNumber locale="de">
        <PhoneNumberCountryList />
        <PhoneNumberControl />
      </PhoneNumber>
    </>
}
```

### Country list

You can use update the list of country to select from.

By default, the list use the `all` preset that displays all supported iso codes. But you can pass a specific subset to limit the possible choices.

For further explanation about country iso code strategy, see .

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>All countries</span>
      <PhoneNumber>
        <PhoneNumberCountryList />
        <PhoneNumberControl />
      </PhoneNumber>
      <span>Subset of countries</span>
      <PhoneNumber countries={['de', 'fr', 'gb', 'it']}>
        <PhoneNumberCountryList />
        <PhoneNumberControl clearable />
      </PhoneNumber>
    </>
}
```

### Form field

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, PhoneNumber, PhoneNumberControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Phone number:      </FormFieldLabel>
      <PhoneNumber>
        <PhoneNumberControl />
      </PhoneNumber>
    </FormField>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Phone Number

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `countries` | `` | No |  | A specific or preset list of country to display in the selector. |
| `country` | `` | No |  | The controlled selected country. |
| `defaultValue` | `` | No |  | The initial phone number value. Use when you don't need to control the value of the phone number. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `i18n` | `` | No |  | Internal translations override. |
| `id` | `` | No |  | The field id. |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `locale` | `` | No |  | The locale used for the translation of the country list and the internal elements. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onCountryChange` | `` | No |  | Callback fired when the country changes. |
| `onValueChange` | `` | No |  | Callback fired when the value changes. |
| `pattern` | `` | No |  | The phone number input expected pattern. |
| `readOnly` | `` | No |  | Whether the component is readonly. |
| `required` | `` | No |  | Whether the component is required. |
| `value` | `` | No |  | The controlled phone number value. |


## Subcomponents


### PhoneNumberControl



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `clearable` | `` | No |  | Whether the clear button is displayed. |
| `loading` | `` | No |  | Whether the component is in loading state. |



### PhoneNumberCountryList



## Examples


### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Phone number:
      </FormFieldLabel>

      <PhoneNumber>
        <PhoneNumberCountryList />

        <PhoneNumberControl />
      </PhoneNumber>
    </FormField>
}
```

### Accessibility I 18 N

```tsx
{
  globals: {
    imports: `import { INPUT_I18N, FormField, FormFieldLabel, PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => <FormField>
      <FormFieldLabel>
        Phone number:
      </FormFieldLabel>

      <PhoneNumber country="fr" defaultValue="06 01 02 03 04" i18n={{
      [INPUT_I18N.clearButton]: 'Clear phone number'
    }}>
        <PhoneNumberCountryList />

        <PhoneNumberControl clearable />
      </PhoneNumber>
    </FormField>
}
```

### Accessibility Label

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Phone number:
      </FormFieldLabel>

      <PhoneNumber>
        <PhoneNumberCountryList />

        <PhoneNumberControl />
      </PhoneNumber>
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <PhoneNumber>
      <PhoneNumberCountryList />

      <PhoneNumberControl />
    </PhoneNumber>
}
```

### Clearable

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <PhoneNumber>
        <PhoneNumberControl clearable />
      </PhoneNumber>

      <PhoneNumber>
        <PhoneNumberCountryList />

        <PhoneNumberControl clearable />
      </PhoneNumber>
    </>
}
```

### Country List

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>All countries</span>

      <PhoneNumber>
        <PhoneNumberCountryList />

        <PhoneNumberControl />
      </PhoneNumber>

      <span>Subset of countries</span>

      <PhoneNumber countries={['de', 'fr', 'gb', 'it']}>
        <PhoneNumberCountryList />

        <PhoneNumberControl clearable />
      </PhoneNumber>
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <PhoneNumber>
      <PhoneNumberControl />
    </PhoneNumber>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <PhoneNumber country={arg.country} disabled={arg.disabled} invalid={arg.invalid} locale={arg.locale} readOnly={arg.readOnly}>
      {arg.withCountries && <PhoneNumberCountryList />}

      <PhoneNumberControl clearable={arg.clearable} />
    </PhoneNumber>,
  argTypes: orderControls({
    clearable: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    country: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'PHONE_NUMBER_COUNTRY_ISO_CODE'
        }
      },
      control: {
        type: 'select'
      },
      options: PHONE_NUMBER_COUNTRY_ISO_CODES
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    locale: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'select'
      },
      options: PHONE_NUMBER_COUNTRY_ISO_CODES
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    withCountries: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    }
  })
}
```

### Disabled

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <PhoneNumber disabled>
        <PhoneNumberControl />
      </PhoneNumber>

      <PhoneNumber disabled>
        <PhoneNumberCountryList />

        <PhoneNumberControl />
      </PhoneNumber>
    </>
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, PhoneNumber, PhoneNumberControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Phone number:
      </FormFieldLabel>

      <PhoneNumber>
        <PhoneNumberControl />
      </PhoneNumber>
    </FormField>
}
```

### Loading

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <PhoneNumber>
        <PhoneNumberControl loading />
      </PhoneNumber>

      <PhoneNumber>
        <PhoneNumberCountryList />

        <PhoneNumberControl loading />
      </PhoneNumber>
    </>
}
```

### Locale

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>Locale "fr"</span>
      <PhoneNumber locale="fr">
        <PhoneNumberCountryList />

        <PhoneNumberControl />
      </PhoneNumber>

      <span>Locale "de"</span>
      <PhoneNumber locale="de">
        <PhoneNumberCountryList />

        <PhoneNumberControl />
      </PhoneNumber>
    </>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <PhoneNumber>
      <PhoneNumberCountryList />

      <PhoneNumberControl />
    </PhoneNumber>
}
```

### Readonly

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { PhoneNumber, PhoneNumberControl, PhoneNumberCountryList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <PhoneNumber readOnly>
        <PhoneNumberControl />
      </PhoneNumber>

      <PhoneNumber readOnly>
        <PhoneNumberCountryList />

        <PhoneNumberControl />
      </PhoneNumber>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    alignItems: 'flex-start'
  }}>
      <PhoneNumber>
        <PhoneNumberControl />
      </PhoneNumber>

      <PhoneNumber>
        <PhoneNumberCountryList />
        <PhoneNumberControl />
      </PhoneNumber>

      <PhoneNumber>
        <PhoneNumberControl loading />
      </PhoneNumber>

      <PhoneNumber disabled>
        <PhoneNumberCountryList />
        <PhoneNumberControl />
      </PhoneNumber>
    </div>
}
```

## React Components

# Popover

_**Popover** component is triggered by click and is used to provide additional information to the user in a new temporary surface that overlays the page_

## Overview

---

A **Popover** will provide additional information to the user in a clear and concise way.

It is commonly used to appear by click, thus crucial information should not be displayed in the **Popover**.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Popover</td></tr><tr><th scope="row">Also known as</th><td>Complex Tooltip</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=49-8447" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/popover" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-popover--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

A **Popover** is used to provide an explanation for a user interface element.

It can include interactive elements such as a button or a link.

### Tooltip vs Popover

-   Both components look similar but a  is displayed on hover and focus while **Popover** is triggered by click.
-   Tooltips are commonly used for shorter explanations, while longer text / complex UIs would suit a popover better.
-   Use a popover when you need to insert interactive elements such as Button.
-   A popover can be dismissed if an action button allows it.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a popover to display contextual information or options that are secondary to the main task |
| - Prefer a popover over a tooltip when the content is longer, interactive, or requires formatting (but keep it focused) |
| - Write a popover content using complete sentences |
| - Ensure the popover is positioned clearly and does not overlap with its trigger element |
| - Use a popover for lightweight content such as field explanations, secondary actions |

| ❌ Don't |
| --- |
| - Don't place critical or mandatory information inside a popover. Users may miss it since it's only revealed on click |
| - Don't overload a popover with rich content like images, videos, or large interactive components. Use a Modal or Drawer instead |
| - Don't trigger a popover from non-obvious elements, the interactive element should clearly indicate it controls a popover |
| - Don't rely on popovers for persistent content since they're meant to be transient and dismissible |
| - Don't use a popover on elements that are already part of another complex interaction |

### Best Practices in Context

1.  **Popover**
2.  **Content**
3.  **Caret tip** - optional
4.  **Trigger**

## Placement

---

**Popover** can be set in a certain position by default around its trigger.

**Popover** has automatic positioning feature. It can detect the edge of the browser so the container always stays visible on a page.

## Behavior

---

By default, a **Popover** is hidden to the user.

It triggers when the user clicks on the **Popover**'s trigger element such as a button.

The **Popover** can be closed or dismissed by clicking anywhere on the page outside the **Popover**'s container.

## Navigation

---

### Focus Management

Depending on the **Popover** trigger used (e.g., a button or a link), refer to that component's documentation for its keyboard interaction.

When the **Popover** is opened, it gains focus automatically. Focus remains within the **Popover** until it is closed.

Closing it returns focus to the trigger element.

### General Keyboard Shortcuts

Pressing Escape closes the currently opened **Popover**.

Pressing Tab moves focus forward through the focusable elements inside the **Popover**.

Pressing Shift + Tab moves focus backward within the **Popover**.

## Accessibility

---

To ensure proper accessibility, **Popover** component must specify its content nature using appropriate ARIA attributes.

### Specify the Popover's content nature

Update the content type on the **Popover Trigger**, which is set by default to `aria-haspopup="dialog"`.

| Content Type | ARIA Attribute |
| --- | --- |
| Menu | `aria-haspopup="menu"` |
| List | `aria-haspopup="listbox"` |
| Tree | `aria-haspopup="tree"` |
| Grid | `aria-haspopup="grid"` |
| Dialog | `aria-haspopup="dialog"` |
| Comment | `aria-details="comment"` |
| Definition | `aria-details="term"` + `role="definition"` |
| Caption | `aria-details="figure"` |
| Footnote | `aria-details="doc-footnote"` |
| Endnote | `aria-details="doc-endnote"` |
| Description | `aria-details="true"` |

### Using aria-popup for a Popover containing a Menu

```jsx
<Popover>
  <PopoverTrigger
    aria-haspopup="menu"
    asChild
  >
    <Button>
      <Icon name="ellipsis-vertical" />
    </Button>
  </PopoverTrigger>
  <PopoverContent
    aria-label="Profile menu"
    withArrow
  >
    <div
      role="menu"
      style={{
        display: 'flex',
        flexDirection: 'column'
      }}
    >
      <Button
        role="menuitem"
        variant="ghost"
      >
        Information      </Button>
      <Button
        role="menuitem"
        variant="ghost"
      >
        Notifications      </Button>
      <Divider
        style={{
          width: '100%'
        }}
       />
      <Button
        color="critical"
        role="menuitem"
        variant="ghost"
      >
        Sign out      </Button>
    </div>
  </PopoverContent>
</Popover>
```

Screen readers will recognize that **Popover** contains a menu and menu items. It indicates that the element can trigger a popup and the nature of this popup.

## React Components

# Popover

## Overview

---

## Anatomy

---

Popover

PopoverContent

PopoverTrigger

---

## Popover

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
autoFocus

 | `boolean` | - | `undefined` | Whether to automatically set focus on the first focusable content within the popover when opened. |
| 

gutterDeprecated

 | `number` | - | `undefined` | Moved to overlayConfig. |
| 

onOpenChange

 | `(detail: PopoverOpenChangeDetail) => void` | - | `undefined` | Callback fired when the popover open state changes. |
| 

onPositionChange

 | `(detail: PopoverPositionChangeDetail) => void` | - | `undefined` | Callback fired when the popover position changes. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the popover. |
| 

overlayConfig

 | `object` | - | `undefined` | The overlay configuration. |
| 

flip

 | `boolean` | - | `-` | Whether to flip the position. |
| 

gutter

 | `number` | - | `-` | The main axis offset or gap between the reference and floating elements. |
| 

position

 | `POPOVER_POSITION` | - | `-` | The popover position around the trigger. |
| 

sameWidth

 | `boolean` | - | `-` | Whether to make the floating element same width as the reference element. |
| 

positionDeprecated

 | `POPOVER_POSITION` | - | `undefined` | Moved to overlayConfig. |
| 

positionerStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |
| 

sameWidthDeprecated

 | `boolean` | - | `undefined` | Moved to overlayConfig. |
| 

triggerId

 | `string` | - | `undefined` | ID of an external trigger element to use in place of the PopoverTrigger component. |

## PopoverContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. |
| 

withArrow

 | `boolean` | - | `false` | Whether the component displays an arrow pointing to the trigger. |

## PopoverTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## Enums

---

### POPOVER_POSITION

-   bottom =`"bottom"`
-   bottomEnd =`"bottom-end"`
-   bottomStart =`"bottom-start"`
-   left =`"left"`
-   leftEnd =`"left-end"`
-   leftStart =`"left-start"`
-   right =`"right"`
-   rightEnd =`"right-end"`
-   rightStart =`"right-start"`
-   top =`"top"`
-   topEnd =`"top-end"`
-   topStart =`"top-start"`

## Interfaces

---

### PopoverOpenChangeDetail

-   `open: boolean`

### PopoverPositionChangeDetail

-   `position: POPOVER_POSITION`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Popover>
      <PopoverTrigger>
        Show popover      </PopoverTrigger>
      <PopoverContent>
        This is the popover content      </PopoverContent>
    </Popover>
}
```

### Custom Trigger

```jsx
{
  globals: {
    imports: `import { Button, Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Popover>
      <PopoverTrigger asChild>
        <Button>
          Custom Trigger        </Button>
      </PopoverTrigger>
      <PopoverContent>
        This is the popover content      </PopoverContent>
    </Popover>
}
```

### Controlled

```jsx
const [isOpen, setIsOpen] = useState(false);
  function togglePopover() {
    setIsOpen(isOpen => !isOpen);
  }
  return <>
      <Button onClick={togglePopover}>
        Toggle popover      </Button>
      <Popover open={isOpen}>
        <PopoverTrigger asChild>
          <Icon name={ICON_NAME.cog} />
        </PopoverTrigger>
        <PopoverContent withArrow>
          This is the popover content        </PopoverContent>
      </Popover>
    </>;
}
```

### Use Trigger Width

```jsx
{
  globals: {
    imports: `import { Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Popover overlayConfig={{
    sameWidth: true
  }}>
      <PopoverTrigger>
        Show popover that will take this width as reference      </PopoverTrigger>
      <PopoverContent>
        The popover content      </PopoverContent>
    </Popover>
}
```

### Grid

```jsx
{
  decorators: [story => <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(3, 1fr)',
    gridTemplateRows: 'repeat(3, 1fr)',
    gap: '20px',
    padding: '50px 150px'
  }}>
      {story()}
    </div>],
  globals: {
    imports: `import { Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Popover overlayConfig={{
      position: 'top-start'
    }}>
        <PopoverTrigger>
          Top Left        </PopoverTrigger>
        <PopoverContent withArrow>
          Top Left popover        </PopoverContent>
      </Popover>
      <Popover overlayConfig={{
      position: 'top'
    }}>
        <PopoverTrigger>
          Top        </PopoverTrigger>
        <PopoverContent withArrow>
          Top popover        </PopoverContent>
      </Popover>
      <Popover overlayConfig={{
      position: 'top-end'
    }}>
        <PopoverTrigger>
          Top Right        </PopoverTrigger>
        <PopoverContent withArrow>
          Top Right popover        </PopoverContent>
      </Popover>
      <Popover overlayConfig={{
      position: 'left'
    }}>
        <PopoverTrigger>
          Middle Left        </PopoverTrigger>
        <PopoverContent withArrow>
          Middle Left popover        </PopoverContent>
      </Popover>
      <div />
      <Popover overlayConfig={{
      position: 'right'
    }}>
        <PopoverTrigger>
          Middle Right        </PopoverTrigger>
        <PopoverContent withArrow>
          Middle Right popover        </PopoverContent>
      </Popover>
      <Popover overlayConfig={{
      position: 'bottom-start'
    }}>
        <PopoverTrigger>
          Bottom Left        </PopoverTrigger>
        <PopoverContent withArrow>
          Bottom Left popover        </PopoverContent>
      </Popover>
      <Popover overlayConfig={{
      position: 'bottom'
    }}>
        <PopoverTrigger>
          Bottom        </PopoverTrigger>
        <PopoverContent withArrow>
          Bottom popover        </PopoverContent>
      </Popover>
      <Popover overlayConfig={{
      position: 'bottom-end'
    }}>
        <PopoverTrigger>
          Bottom Right        </PopoverTrigger>
        <PopoverContent withArrow>
          Bottom Right popover        </PopoverContent>
      </Popover>
    </>
}
```

### Z index

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, POPOVER_POSITION, Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>Default Z-axis order:</span>
      <Popover open>
        <PopoverTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </PopoverTrigger>
        <PopoverContent withArrow>
          Back        </PopoverContent>
      </Popover>
      <Popover open>
        <PopoverTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </PopoverTrigger>
        <PopoverContent withArrow>
          Front        </PopoverContent>
      </Popover>
      <br />
      <span>Custom Z-axis order:</span>
      <Popover open overlayConfig={{
      position: POPOVER_POSITION.bottom
    }} positionerStyle={{
      zIndex: 'calc(var(--ods-theme-overlay-z-index) + 1)'
    }}>
        <PopoverTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </PopoverTrigger>
        <PopoverContent withArrow>
          Front        </PopoverContent>
      </Popover>
      <Popover open overlayConfig={{
      position: POPOVER_POSITION.bottom
    }}>
        <PopoverTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </PopoverTrigger>
        <PopoverContent withArrow>
          Back        </PopoverContent>
      </Popover>
    </>
}
```

## Recipes

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

## React Components/Popover

## Subcomponents


### PopoverContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. |
| `withArrow` | `` | No | false | Whether the component displays an arrow pointing to the trigger. |



### PopoverTrigger



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `asChild` | `` | No |  | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |


## Examples


### Accessibility With Menu

```tsx
{
  globals: {
    imports: `import { BUTTON_COLOR, BUTTON_VARIANT, ICON_NAME, Button, Divider, Icon, Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Popover>
      <PopoverTrigger aria-haspopup="menu" asChild>
        <Button>
          <Icon name={ICON_NAME.ellipsisVertical} />
        </Button>
      </PopoverTrigger>

      <PopoverContent aria-label="Profile menu" withArrow>
        <div role="menu" style={{
        display: 'flex',
        flexDirection: 'column'
      }}>
          <Button role="menuitem" variant={BUTTON_VARIANT.ghost}>
            Information
          </Button>

          <Button role="menuitem" variant={BUTTON_VARIANT.ghost}>
            Notifications
          </Button>

          <Divider style={{
          width: '100%'
        }} />

          <Button color={BUTTON_COLOR.critical} role="menuitem" variant={BUTTON_VARIANT.ghost}>
            Sign out
          </Button>
        </div>
      </PopoverContent>
    </Popover>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Popover open={true} overlayConfig={{
    flip: false,
    position: POPOVER_POSITION.top
  }}>
      <PopoverTrigger asChild>
        <Button>
          Popover trigger
        </Button>
      </PopoverTrigger>

      <PopoverContent createPortal={false}>
        This is the popover content
      </PopoverContent>
    </Popover>
}
```

### Controlled

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Button, Icon, Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [isOpen, setIsOpen] = useState(false);
    function togglePopover() {
      setIsOpen(isOpen => !isOpen);
    }
    return <>
        <Button onClick={togglePopover}>
          Toggle popover
        </Button>

        <Popover open={isOpen}>
          <PopoverTrigger asChild>
            <Icon name={ICON_NAME.cog} />
          </PopoverTrigger>

          <PopoverContent withArrow>
            This is the popover content
          </PopoverContent>
        </Popover>
      </>;
  }
}
```

### Custom Trigger

```tsx
{
  globals: {
    imports: `import { Button, Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Popover>
      <PopoverTrigger asChild>
        <Button>
          Custom Trigger
        </Button>
      </PopoverTrigger>

      <PopoverContent>
        This is the popover content
      </PopoverContent>
    </Popover>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Popover>
      <PopoverTrigger>
        Show popover
      </PopoverTrigger>

      <PopoverContent>
        This is the popover content
      </PopoverContent>
    </Popover>
}
```

### Demo

```tsx
{
  parameters: {
    layout: 'centered'
  },
  render: (arg: DemoArg) => <Popover overlayConfig={{
    gutter: arg.gutter,
    position: arg.position,
    sameWidth: arg.sameWidth
  }}>
      <PopoverTrigger>
        Show popover
      </PopoverTrigger>

      <PopoverContent withArrow={arg.withArrow}>
        {arg.content}
      </PopoverContent>
    </Popover>,
  argTypes: orderControls({
    content: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    gutter: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'number'
        }
      },
      control: 'number'
    },
    position: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'POPOVER_POSITION'
        }
      },
      control: {
        type: 'select'
      },
      options: POPOVER_POSITIONS
    },
    sameWidth: {
      table: {
        category: CONTROL_CATEGORY.design
      },
      control: {
        type: 'boolean'
      }
    },
    withArrow: {
      table: {
        category: CONTROL_CATEGORY.design,
        defaultValue: {
          summary: false
        },
        type: {
          summary: 'boolean'
        }
      },
      control: {
        type: 'boolean'
      }
    }
  }),
  args: {
    content: 'My popover content'
  }
}
```

### Grid

```tsx
{
  decorators: [story => <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(3, 1fr)',
    gridTemplateRows: 'repeat(3, 1fr)',
    gap: '20px',
    padding: '50px 150px'
  }}>
      {story()}
    </div>],
  globals: {
    imports: `import { Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Popover overlayConfig={{
      position: 'top-start'
    }}>
        <PopoverTrigger>
          Top Left
        </PopoverTrigger>
        <PopoverContent withArrow>
          Top Left popover
        </PopoverContent>
      </Popover>

      <Popover overlayConfig={{
      position: 'top'
    }}>
        <PopoverTrigger>
          Top
        </PopoverTrigger>
        <PopoverContent withArrow>
          Top popover
        </PopoverContent>
      </Popover>

      <Popover overlayConfig={{
      position: 'top-end'
    }}>
        <PopoverTrigger>
          Top Right
        </PopoverTrigger>
        <PopoverContent withArrow>
          Top Right popover
        </PopoverContent>
      </Popover>

      <Popover overlayConfig={{
      position: 'left'
    }}>
        <PopoverTrigger>
          Middle Left
        </PopoverTrigger>
        <PopoverContent withArrow>
          Middle Left popover
        </PopoverContent>
      </Popover>

      <div />

      <Popover overlayConfig={{
      position: 'right'
    }}>
        <PopoverTrigger>
          Middle Right
        </PopoverTrigger>
        <PopoverContent withArrow>
          Middle Right popover
        </PopoverContent>
      </Popover>

      <Popover overlayConfig={{
      position: 'bottom-start'
    }}>
        <PopoverTrigger>
          Bottom Left
        </PopoverTrigger>
        <PopoverContent withArrow>
          Bottom Left popover
        </PopoverContent>
      </Popover>

      <Popover overlayConfig={{
      position: 'bottom'
    }}>
        <PopoverTrigger>
          Bottom
        </PopoverTrigger>
        <PopoverContent withArrow>
          Bottom popover
        </PopoverContent>
      </Popover>

      <Popover overlayConfig={{
      position: 'bottom-end'
    }}>
        <PopoverTrigger>
          Bottom Right
        </PopoverTrigger>
        <PopoverContent withArrow>
          Bottom Right popover
        </PopoverContent>
      </Popover>
    </>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Popover>
      <PopoverTrigger asChild>
        <Button>
          Show popover
        </Button>
      </PopoverTrigger>

      <PopoverContent>
        This is the popover content
      </PopoverContent>
    </Popover>
}
```

### Same Width

```tsx
{
  globals: {
    imports: `import { Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Popover overlayConfig={{
    sameWidth: true
  }}>
      <PopoverTrigger>
        Show popover that will take this width as reference
      </PopoverTrigger>

      <PopoverContent>
        The popover content
      </PopoverContent>
    </Popover>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'row wrap',
    gap: '12px'
  }}>
      <Popover>
        <PopoverTrigger asChild>
          <Button>Default</Button>
        </PopoverTrigger>
        <PopoverContent createPortal={false}>This is the popover content</PopoverContent>
      </Popover>

      <Popover>
        <PopoverTrigger asChild>
          <Button>With Arrow</Button>
        </PopoverTrigger>
        <PopoverContent createPortal={false} withArrow>This is the popover content</PopoverContent>
      </Popover>
    </div>
}
```

### Z Index

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, POPOVER_POSITION, Popover, PopoverContent, PopoverTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>Default Z-axis order:</span>

      <Popover open>
        <PopoverTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </PopoverTrigger>

        <PopoverContent withArrow>
          Back
        </PopoverContent>
      </Popover>

      <Popover open>
        <PopoverTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </PopoverTrigger>

        <PopoverContent withArrow>
          Front
        </PopoverContent>
      </Popover>

      <br />

      <span>Custom Z-axis order:</span>

      <Popover open overlayConfig={{
      position: POPOVER_POSITION.bottom
    }} positionerStyle={{
      zIndex: 'calc(var(--ods-theme-overlay-z-index) + 1)'
    }}>
        <PopoverTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </PopoverTrigger>

        <PopoverContent withArrow>
          Front
        </PopoverContent>
      </Popover>

      <Popover open overlayConfig={{
      position: POPOVER_POSITION.bottom
    }}>
        <PopoverTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </PopoverTrigger>

        <PopoverContent withArrow>
          Back
        </PopoverContent>
      </Popover>
    </>
}
```

## React Components

# Progress Bar

_A horizontal bar indicating the current completion status of a long-running task, usually updated continuously as the task progresses, instead of in discrete steps._

## Overview

---

The **Progress Bar** component is used to indicate the progress of a task or process to users. It visually represents the completion percentage, providing users with feedback on the status of ongoing operations, such as file uploads, downloads, or form submissions.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Progress Bar</td></tr><tr><th scope="row">Also known as</th><td>Progress, Progress Loader, Preloader, Loading Bar, Progress Indicator</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=49-8976" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/progress-bar" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-progress-bar--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Progress Bar** component is only used in process progression, such as submitting, uploading or saving items.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Progress Bar to represent linear, quantifiable progression in processes like uploads or submissions |
| - Prefer Progress Bar when the completion state is measurable (e.g. 0–100%), and users benefit from visual feedback on progress |
| - Use clear labels or indicators (like percentage or step descriptions) if it adds clarity to the task |
| - Pair the Progress Bar with contextual messaging to clarify what is being processed and what happens next |

| ❌ Don't |
| --- |
| - Don't use a Progress Bar to visualize static data or compare values, use a chart or graph instead |
| - Don't display all three labels (start value, end value, current value) at the same time |
| - Don't rely solely on color to communicate progress, include text or value where helpful |
| - Don't use Progress Bar for instantaneous actions where progress feedback is not meaningful, use a spinner instead |

### Best Practices in Context

1.  **Progress Bar**
2.  **Track**
3.  **Progress fill**

## Placement

---

**Progress Bar** is usually centered in its container, and can be stretched to match the container width if necessary.

## Behavior

---

**Progress Bar** is filled from the minimum to the maximum value, depending on the progress described.

## Navigation

---

The **Progress Bar** component is non-interactive and does not receive keyboard focus. It is purely visual and used to indicate progress status without affecting keyboard navigation.

## Accessibility

---

**Progress Bar** component should be properly identified and structured to ensure it is accessible to assistive technologies.

### Linking the Progress Bar to loading details

To ensure the **Progress Bar** is correctly recognized, an [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) should be added to explicitly identify it.

```jsx
<ProgressBar aria-label="Converting" />
```

Screen readers will announce the label, the value and the progress bar.

## React Components

# Progress Bar

## Overview

---

## Anatomy

---

ProgressBar

---

## ProgressBar

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [progress attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/progress#attributes) . |
| 
max

 | `string | number` | - | `100` | The maximum value of the progress bar. |
| 

value

 | `string | number | 'indeterminate'` | - | `0` | The current value of the progress bar |

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-progress-bar-border-radius | var(--ods-theme-border-radius) | 
 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { ProgressBar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ProgressBar />
}
```

### Max

```jsx
{
  globals: {
    imports: `import { ProgressBar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ProgressBar max="500" value="50" />
}
```

### Value

```jsx
{
  globals: {
    imports: `import { ProgressBar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ProgressBar value="50" />
}
```

### Indeterminate

```jsx
<ProgressBar value="indeterminate" />
```

## Recipes

---

No recipe defined for now.

## React Components/Progress Bar

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `max` | `` | No | 100 | The maximum value of the progress bar. |
| `value` | `` | No | 0 | The current value of the progress bar |


## Examples


### Accessibility Label

```tsx
{
  globals: {
    imports: `import { ProgressBar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ProgressBar aria-label="Converting" />
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <ProgressBar value="50" />
}
```

### Default

```tsx
{
  globals: {
    imports: `import { ProgressBar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ProgressBar />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    max: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'number'
        }
      },
      control: 'number'
    },
    value: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'number'
        }
      },
      control: 'number'
    }
  })
}
```

### Indeterminate

```tsx
{
  globals: {
    imports: `import { ProgressBar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ProgressBar value="indeterminate" />
}
```

### Max

```tsx
{
  globals: {
    imports: `import { ProgressBar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ProgressBar max="500" value="50" />
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  render: ({}) => <ProgressBar value="50" />
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    width: '300px'
  }}>
      <ProgressBar />
      <ProgressBar value="25" />
      <ProgressBar value="50" />
      <ProgressBar value="75" />
      <ProgressBar max="500" value="50" />
    </div>
}
```

### Value

```tsx
{
  globals: {
    imports: `import { ProgressBar } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ProgressBar value="50" />
}
```

## React Components

# Prompt Input

_The **Prompt Input** component is a composable text input with a submit button, designed for conversational and AI-driven interfaces._

## Overview

---

The **Prompt Input** component is a multi-line textarea with an integrated submit button, designed to handle user input and submission.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Prompt Input</td></tr><tr><th scope="row">Also known as</th><td>Prompt textarea, Chat input, Submissible text</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/mwZtfuQ9nzv6fY0dfez4NZ/ODS---UI-Kit?node-id=18830-7160&amp;p=f&amp;t=321I8hUuyf5gg2uQ-0 " target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/prompt-input" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Prompt Input** component is commonly used for user input and submission in chat interfaces, messaging applications, and other interactive systems.

It is commonly used for:

-   Chat interfaces to allow users to send messages to each other
-   Virtual assistants to allow users to input queries or commands
-   Content creation tools to allow users to input text or upload files for content creation
-   AI-powered writing tools to allow users to input prompts and receive generated text
-   Customer support platforms to allow users to input questions or issues and receive support

### Dos & Don'ts

| ✅ Do |
| --- |
| - Provide a placeholder text to guide users on what to input |
| - Use the attachment upload feature to allow users to add files to their messages |

| ❌ Don't |
| --- |
| - Disable the submit button without a clear reason (e.g., empty input, max length exceeded) |
| - Allow submission while a file is still uploading |
| - Use the component as a replacement for a standard textarea without considering the implications of the integrated submit button |
| - Forget to handle errors and exceptions properly, such as when a user attempts to upload a file that exceeds the maximum allowed size |

### Best Practices in Context

1.  **Prompt Input**
2.  **File attachments (if attached)** - optional
3.  **File upload button** - optional
4.  **Input text area**
5.  **Send button**

## Placement

---

The **Prompt Input** is typically anchored at the bottom of a conversational view, spanning the full width of the content area.

It should remain visible and accessible at all times during an active conversation, without being obscured by the message list above it.

## Behavior

---

### Submission

The submit button is enabled when the input is not empty and file stack is empty and the max length has not been exceeded. When the submit button is clicked, the component submits the user's input.

After submission, input is being cleared.

### Processing State

When the component is in a processing or generating state, the input can be locked (configurable), and the submit button is replaced with a "stop" button.

### Attachments

The component allows users to upload attachments, which are displayed in a preview area with a clear/remove button. The attachment upload feature can be triggered by a file upload trigger.

## Navigation

---

### Focus Management

When the component receives focus, it should focus on the textarea. When the user interacts with the attachment upload feature, focus should be moved to the file upload trigger.

### General Keyboard Shortcuts

Pressing Tabmoves focus forward to the next interactive element.

Pressing Shift + Tab moves focus backward to the previous interactive element.

Pressing Enter submits the user's input when the submit button or the textarea is focused.

Pressing Shift + Enter creates a new line in the user's input.

Pressing Escape cancels the submission and closes any open attachment upload triggers.

## Accessibility

---

### Textarea labelling

Because the **Prompt Input** does not render a visible label by default, you must supply an accessible name for the textarea.

Use the `aria-label` prop on `PromptInputTextControl` to give screen readers a meaningful description of the field.

```jsx
<PromptInput>
  <PromptInputControls>
    <PromptInputTextControl aria-label="Ask someone about something…" />
    <PromptInputSendButton />
  </PromptInputControls>
</PromptInput>
```

### Button labelling

-   The file upload button and the send button don't hold accessible text by default, therefore they must carry an `aria-label` (e.g. `"Attach file"`, `"Send request"`, …) to be announced correctly by assistive technologies.

```jsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput>
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Send request" />
      </PromptInputControls>
    </PromptInput>
}
```

-   As the send button displays a loading spinner when the component is in the `loading` state, ensure the button's `aria-label` is updated to convey the loading state to users who can't rely on visual cues.

```jsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputTextControl, PromptInputSendButton, PromptInputFileUploadButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput loading>
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Request is processing" />
      </PromptInputControls>
    </PromptInput>
}
```

## React Components

# Prompt Input

## Overview

---

## Anatomy

---

PromptInput

PromptInputControls

PromptInputFiles

PromptInputFileUploadButton

PromptInputSendButton

PromptInputTextControl

---

## PromptInput

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultValue

 | `string` | - | `undefined` | The initial textarea value. Use when you don't need to control the value of the textarea. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

fileCollection

 | `File[]` | - | `undefined` | Array of the files associated with the prompt input. |
| 

loading

 | `boolean` | - | `undefined` | Whether the component is in loading state. |
| 

name

 | `string` | - | `undefined` | The name of the textarea form element. Useful for form submission. |
| 

onFileChange

 | `(detail: PromptInputFileChangeDetails) => void` | - | `undefined` | Callback fired when a file change is triggered on the file-upload button. |
| 

onInputSubmit

 | `(detail: PromptInputInputSubmitDetails) => void` | - | `undefined` | Callback fired when the prompt input is submitted. |
| 

onValueChange

 | `(detail: PromptInputValueChangeDetails) => void` | - | `undefined` | Callback fired when the prompt input text value changes. |
| 

readOnly

 | `boolean` | - | `undefined` | Whether the component is readonly. |
| 

value

 | `string` | - | `undefined` | The controlled textarea value. |

## PromptInputControls

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## PromptInputFiles

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## PromptInputFileUploadButton

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
accept

 | `HTMLInputElement['accept']` | - | `undefined` | Expected file type in file upload controls |
| 

color

 | `BUTTON_COLOR` | - | `undefined` | The color preset to use. |
| 

loading

 | `boolean` | - | `undefined` | Whether the component is in loading state, disabling it. |
| 

multiple

 | `HTMLInputElement['multiple']` | - | `undefined` | Whether multiple files can be selected at once |
| 

size

 | `BUTTON_SIZE` | - | `undefined` | The size preset to use. |
| 

variant

 | `BUTTON_VARIANT` | - | `undefined` | The variant preset to use. |

## PromptInputSendButton

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
color

 | `BUTTON_COLOR` | - | `undefined` | The color preset to use. |
| 

loading

 | `boolean` | - | `undefined` | Whether the component is in loading state, disabling it. |
| 

size

 | `BUTTON_SIZE` | - | `undefined` | The size preset to use. |
| 

variant

 | `BUTTON_VARIANT` | - | `undefined` | The variant preset to use. |

## PromptInputTextControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [textarea attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/textarea#attributes) . |
| 
invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

placeholder

 | `string` | - | `undefined` | - |

## Interfaces

---

### PromptInputFileChangeDetails

-   `files: File[]`

### PromptInputInputSubmitDetails

-   `inputValue: string`

### PromptInputValueChangeDetails

-   `inputValue: string`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-prompt-input-border-color | var(--ods-color-form-element-border-default) | 
 |
| --ods-prompt-input-border-color-focus | var(--ods-color-form-element-border-focus-default) | 

 |
| --ods-prompt-input-border-color-hover | var(--ods-color-form-element-border-hover-default) | 

 |
| --ods-prompt-input-border-radius | var(--ods-theme-border-radius) | 

 |
| --ods-prompt-input-control-border-color | transparent | 

 |
| --ods-prompt-input-control-line-height | 1.375em | 

 |
| --ods-prompt-input-control-max-lines | 5 | 

 |
| --ods-prompt-input-control-min-lines | 1 | 

 |
| --ods-prompt-input-control-padding-horizontal | var(--ods-theme-input-padding-horizontal) | 

 |
| --ods-prompt-input-control-padding-vertical | calc(var(--ods-theme-padding-vertical) / 2) | 

 |

## Examples

---

### Default

```jsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput>
      <PromptInputControls>
        <PromptInputFileUploadButton />
        <PromptInputTextControl />
        <PromptInputSendButton />
      </PromptInputControls>
    </PromptInput>
}
```

### Disabled

```jsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput disabled defaultValue="Nobody’s here to answer…">
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach a file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Send message" />
      </PromptInputControls>
    </PromptInput>
}
```

### Readonly

```jsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput readOnly defaultValue="This is a read-only prompt input.">
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach a file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Send message" />
      </PromptInputControls>
    </PromptInput>
}
```

### Loading

```jsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput defaultValue="“Someone” is processing your request…" loading>
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach a file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Send message" />
      </PromptInputControls>
    </PromptInput>
}
```

### With files

```jsx
const fakePdfFile = new File(["foo"], "loading-pdf-file.pdf", {
    type: "application/pdf"
  });
  const fakeTextFile = new File(["bar"], "text-file.txt", {
    type: "text/plain"
  });
  return <PromptInput fileCollection={[fakePdfFile, fakeTextFile]}>
      <PromptInputFiles>
        <FileThumbnail file={fakePdfFile} progress={45} />
        <FileThumbnail file={fakeTextFile} />
      </PromptInputFiles>
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach a file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Send message" />
      </PromptInputControls>
    </PromptInput>;
}
```

### In form field

```jsx
const maxLength = 60;
  const [inputValue, setInputValue] = useState("Some text that is almost hitting the length limit...");
  return <FormField invalid={inputValue?.length > maxLength}>
      <FormFieldLabel>Here is a prompt input inside a form field:</FormFieldLabel>
      <PromptInput defaultValue={inputValue} name="prompt-input-textArea" onValueChange={({
      inputValue    }) => setInputValue(inputValue)}>
        <PromptInputControls>
          <PromptInputFileUploadButton aria-label="Attach a file" />
          <div style={{
          display: "flex",
          flexDirection: "column",
          flexGrow: 1,
          rowGap: "var(--ods-theme-row-gap)"
        }}>
            <PromptInputTextControl aria-label="Ask someone about something…" />
            <Divider style={{
            width: "100%"
          }} />
            <FormFieldHelper>
              <Text preset={TEXT_PRESET.caption}>
                {inputValue?.length} / {maxLength}
              </Text>
            </FormFieldHelper>
          </div>
          <PromptInputSendButton aria-label="Send message" />
        </PromptInputControls>
      </PromptInput>
      <FormFieldError>Character limit exceeded</FormFieldError>
    </FormField>;
}
```

## Recipes

---

Chat

Assistant2:58 PM

Welcome to the Chat recipe. Feel free to test the UI behavior by typing anything on your mind below.

## React Components/Prompt Input

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultValue` | `` | No |  | The initial textarea value. Use when you don't need to control the value of the textarea. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `fileCollection` | `` | No |  | Array of the files associated with the prompt input. |
| `value` | `` | No |  | The controlled textarea value. |
| `loading` | `` | No |  | Whether the component is in loading state. |
| `name` | `` | No |  | The name of the textarea form element. Useful for form submission. |
| `onInputSubmit` | `` | No |  | Callback fired when the prompt input is submitted. |
| `onValueChange` | `` | No |  | Callback fired when the prompt input text value changes. |
| `onFileChange` | `` | No |  | Callback fired when a file change is triggered on the file-upload button. |
| `readOnly` | `` | No |  | Whether the component is readonly. |


## Subcomponents


### PromptInputControls




### PromptInputFiles




### PromptInputFileUploadButton



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No |  | @type=BUTTON_COLOR The color preset to use. |
| `loading` | `` | No |  | Whether the component is in loading state, disabling it. |
| `size` | `` | No |  | The size preset to use. |
| `variant` | `` | No |  | The variant preset to use. |
| `accept` | `` | No |  | Expected file type in file upload controls |
| `multiple` | `` | No |  | Whether multiple files can be selected at once |



### PromptInputSendButton



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No |  | @type=BUTTON_COLOR The color preset to use. |
| `loading` | `` | No |  | Whether the component is in loading state, disabling it. |
| `size` | `` | No |  | The size preset to use. |
| `variant` | `` | No |  | The variant preset to use. |



### PromptInputTextControl



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `invalid` | `` | No |  | Whether the component is in error state. |
| `placeholder` | `` | No |  |  |


## Examples


### Accessibility Aria Label

```tsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput>
      <PromptInputControls>
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton />
      </PromptInputControls>
    </PromptInput>
}
```

### Accessibility Buttons Labels

```tsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput>
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Send request" />
      </PromptInputControls>
    </PromptInput>
}
```

### Accessibility Loading State

```tsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputTextControl, PromptInputSendButton, PromptInputFileUploadButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput loading>
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Request is processing" />
      </PromptInputControls>
    </PromptInput>
}
```

### Anatomy Tech

```tsx
{
  tags: ["!dev"],
  render: ({}) => {
    const uploadedFile = new File(["foo"], "a-default-text-file.txt", {
      type: "text/plain"
    });
    return <div style={{
      minWidth: '500px'
    }}>
        <PromptInput>
          <PromptInputFiles>
            <FileThumbnail file={uploadedFile} />
          </PromptInputFiles>
          <PromptInputControls>
            <PromptInputFileUploadButton />
            <PromptInputTextControl placeholder="This is where you can ask about something…" />
            <PromptInputSendButton />
          </PromptInputControls>
        </PromptInput>
      </div>;
  }
}
```

### Default

```tsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput>
      <PromptInputControls>
        <PromptInputFileUploadButton />
        <PromptInputTextControl />
        <PromptInputSendButton />
      </PromptInputControls>
    </PromptInput>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    disabled: {
      control: "boolean",
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    loading: {
      control: "boolean",
      table: {
        category: CONTROL_CATEGORY.general
      }
    },
    readOnly: {
      control: "boolean",
      table: {
        category: CONTROL_CATEGORY.general
      }
    }
  }),
  render: arg => <PromptInput loading={arg.loading} readOnly={arg.readOnly} disabled={arg.disabled}>
      <PromptInputControls>
        <PromptInputFileUploadButton />
        <PromptInputTextControl placeholder="Type your message here..." />
        <PromptInputSendButton />
      </PromptInputControls>
    </PromptInput>
}
```

### Disabled

```tsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput disabled defaultValue="Nobody’s here to answer…">
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach a file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Send message" />
      </PromptInputControls>
    </PromptInput>
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { Divider, FormField, FormFieldError, FormFieldHelper, FormFieldLabel, PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton, Text, TEXT_PRESET } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ["!dev"],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const maxLength = 60;
    const [inputValue, setInputValue] = useState("Some text that is almost hitting the length limit...");
    return <FormField invalid={inputValue?.length > maxLength}>
        <FormFieldLabel>Here is a prompt input inside a form field:</FormFieldLabel>
        <PromptInput defaultValue={inputValue} name="prompt-input-textArea" onValueChange={({
        inputValue
      }) => setInputValue(inputValue)}>
          <PromptInputControls>
            <PromptInputFileUploadButton aria-label="Attach a file" />
            <div style={{
            display: "flex",
            flexDirection: "column",
            flexGrow: 1,
            rowGap: "var(--ods-theme-row-gap)"
          }}>
              <PromptInputTextControl aria-label="Ask someone about something…" />
              <Divider style={{
              width: "100%"
            }} />
              <FormFieldHelper>
                <Text preset={TEXT_PRESET.caption}>
                  {inputValue?.length} / {maxLength}
                </Text>
              </FormFieldHelper>
            </div>
            <PromptInputSendButton aria-label="Send message" />
          </PromptInputControls>
        </PromptInput>
        <FormFieldError>Character limit exceeded</FormFieldError>
      </FormField>;
  }
}
```

### Loading

```tsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput defaultValue="“Someone” is processing your request…" loading>
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach a file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Send message" />
      </PromptInputControls>
    </PromptInput>
}
```

### Overview

```tsx
{
  tags: ["!dev"],
  render: ({}) => {
    const [uploadedFiles, setUploadedFiles] = useState<File[]>([new File(["foo"], "a-default-text-file.txt", {
      type: "text/plain"
    }), new File(["foo"], "another-default-text-file.txt", {
      type: "text/plain"
    })]);
    const [isLoading, setIsLoading] = useState(false);
    const handleFileChange = ({
      files
    }: {
      files: File[];
    }): void => {
      setUploadedFiles(prev => [...prev, ...files]);
    };
    const handleFileRemove = (fileToRemove: File): void => {
      setUploadedFiles(prev => prev.filter(file => file !== fileToRemove));
    };
    const handleInputSubmit = (): void => {
      setIsLoading(true);
      setTimeout(() => {
        setIsLoading(false);
      }, 1500);
    };
    return <PromptInput loading={isLoading} onFileChange={handleFileChange} onInputSubmit={handleInputSubmit} fileCollection={uploadedFiles}>
        <PromptInputFiles>
          {uploadedFiles?.map((file, index) => <FileThumbnail key={index} file={file} onFileRemove={() => handleFileRemove(file)} />)}
        </PromptInputFiles>
        <PromptInputControls>
          <PromptInputFileUploadButton multiple aria-description="description" aria-label="label" />
          <PromptInputTextControl aria-label="Ask someone about something…" placeholder="Ask someone about something…" />
          <PromptInputSendButton aria-label={isLoading ? "Sending message…" : "Send message"} />
        </PromptInputControls>
      </PromptInput>;
  }
}
```

### Read Only

```tsx
{
  tags: ["!dev"],
  globals: {
    imports: "import { PromptInput, PromptInputControls, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  render: ({}) => <PromptInput readOnly defaultValue="This is a read-only prompt input.">
      <PromptInputControls>
        <PromptInputFileUploadButton aria-label="Attach a file" />
        <PromptInputTextControl aria-label="Ask someone about something…" />
        <PromptInputSendButton aria-label="Send message" />
      </PromptInputControls>
    </PromptInput>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: "fullscreen"
  },
  tags: ["!dev"],
  render: ({}) => <div style={{
    display: "flex",
    flexDirection: "column",
    gap: "12px"
  }}>
      <PromptInput>
        <PromptInputFiles>
          <FileThumbnail file={new File(["foo"], "file1.pdf", {
          type: "application/pdf"
        })} progress={45} />
          <FileThumbnail file={new File(["bar"], "file2.txt", {
          type: "text/plain"
        })} />
        </PromptInputFiles>
        <PromptInputControls>
          <PromptInputFileUploadButton aria-label="Attach file" />
          <PromptInputTextControl aria-label="Ask someone about something…" placeholder="Ask someone about something…" />
          <PromptInputSendButton aria-label="Send request" />
        </PromptInputControls>
      </PromptInput>
      <PromptInput defaultValue="This is a default value">
        <PromptInputControls>
          <PromptInputFileUploadButton aria-label="Attach file" />
          <PromptInputTextControl aria-label="Ask someone about something…" placeholder="Ask someone about something…" />
          <PromptInputSendButton aria-label="Send request" />
        </PromptInputControls>
      </PromptInput>
      <PromptInput disabled defaultValue="This is a default value in a disabled prompt input.">
        <PromptInputControls>
          <PromptInputFileUploadButton aria-label="Attach file" />
          <PromptInputTextControl aria-label="Ask someone about something…" placeholder="Ask someone about something…" />
          <PromptInputSendButton aria-label="Request is processing" />
        </PromptInputControls>
      </PromptInput>
      <PromptInput loading>
        <PromptInputControls>
          <PromptInputFileUploadButton aria-label="Attach file" />
          <PromptInputTextControl aria-label="Ask someone about something…" placeholder="Placeholder in loading prompt input…" />
          <PromptInputSendButton aria-label="Request is processing" />
        </PromptInputControls>
      </PromptInput>
      <PromptInput readOnly defaultValue="This is a read-only prompt input.">
        <PromptInputControls>
          <PromptInputTextControl aria-label="Ask someone about something…" placeholder="Ask someone about something…" />
          <PromptInputSendButton aria-label="Request is processing" />
        </PromptInputControls>
      </PromptInput>
    </div>
}
```

### With Files

```tsx
{
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  globals: {
    imports: "import { FileThumbnail, PromptInput, PromptInputControls, PromptInputFiles, PromptInputFileUploadButton, PromptInputTextControl, PromptInputSendButton } from '@ovhcloud/ods-react';"
  },
  tags: ["!dev"],
  render: ({}) => {
    const fakePdfFile = new File(["foo"], "loading-pdf-file.pdf", {
      type: "application/pdf"
    });
    const fakeTextFile = new File(["bar"], "text-file.txt", {
      type: "text/plain"
    });
    return <PromptInput fileCollection={[fakePdfFile, fakeTextFile]}>
        <PromptInputFiles>
          <FileThumbnail file={fakePdfFile} progress={45} />
          <FileThumbnail file={fakeTextFile} />
        </PromptInputFiles>
        <PromptInputControls>
          <PromptInputFileUploadButton aria-label="Attach a file" />
          <PromptInputTextControl aria-label="Ask someone about something…" />
          <PromptInputSendButton aria-label="Send message" />
        </PromptInputControls>
      </PromptInput>;
  }
}
```

## React Components

# Quantity

_**Quantity** is a component used to enter and modify a numeric value in a responsive and easy way_

## Overview

---

The **Quantity** component provides a user-friendly interface for selecting or entering numerical values.

It includes an **Input** field combined with increment and decrement **Buttons** to adjust the value easily.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Quantity</td></tr><tr><th scope="row">Also known as</th><td>Numeric, Number input, Amount</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=49-9160" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/quantity" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-quantity--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

**Quantity** is used to quickly adjust with a few clicks a numeric value within defined parameters.

Limits can be set as minimum or maximum value. In that case, the numeric value cannot exceed the defined boundaries.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the Quantity component for small, incremental adjustments where users are likely to increase or decrease values with just a few clicks |
| - Always allow users to manually input a number directly into the field, not just through the increment/decrement buttons |
| - Set appropriate min/max limits to prevent invalid or unintended values |
| - Provide contextual labels or grouping when needed to clarify what the quantity refers to |

| ❌ Don't |
| --- |
| - Don't use the Quantity component for large numeric changes that would require excessive clicking. Prefer a freeform numeric input or slider in those cases |
| - Don't use the component outside of quantitative contexts: it should only be used to adjust measurable numeric values (e.g., quantity, amount) |

### Best Practices in Context

1.  **Quantity**
2.  **Decrement button**
3.  **Input field**
4.  **Increment button**

## Placement

---

**Quantity** can be used to let users easily change an amount of items, for example, or similar situations.

It should be used with numeric **Input** that are in a fairly narrow range.

## Behavior

---

**Quantity** numeric field can be edited directly by the user.

The user can also decrease or increase the value by clicking on the **Buttons**.

In case that limits have been set to minimum and maximum value, corresponding control will be disabled when this value has been reached.

Each item of the **Quantity** component can be focused; the whole component can be disabled at once.

## Navigation

---

### Focus Management

When the **Quantity** component is focused, focus is automatically set to the input if not disabled.

If the **Input** field is read-only, it can still receive focus but cannot be edited.

### General Keyboard Shortcuts

Pressing Tab moves focus forward the input field.

Pressing Shift + Tab moves focus backward through the interactive elements.

Pressing Arrow Up when focused in the Input field increases the value.

Pressing Arrow Down when focused in the Input field decreases the value.

## Accessibility

---

This component complies with the [Spinbutton WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/)

### Always provide an explicit label

Every **Quantity** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Number of CPUs:

```jsx
<FormField>
  <FormFieldLabel>
    Number of CPUs:  </FormFieldLabel>
  <Quantity
    max={10}
    min={0}
  >
    <QuantityControl>
      <QuantityInput />
    </QuantityControl>
  </Quantity>
</FormField>
```

Screen readers will announce the label and the quantity field.

## React Components

# Quantity

## Overview

---

## Anatomy

---

Quantity

QuantityControl

QuantityInput

---

## Quantity

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultValue

 | `string` | - | `undefined` | The initial quantity value. Use when you don't need to control the value of the quantity. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

max

 | `number` | - | `undefined` | The maximum quantity that can be selected. |
| 

min

 | `number` | - | `undefined` | The minimum quantity that can be selected. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onValueChange

 | `(detail: QuantityValueChangeDetail) => void` | - | `undefined` | Callback fired when the value changes. |
| 

readOnly

 | `boolean` | - | `undefined` | Whether the component is readonly. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. |
| 

step

 | `number` | - | `undefined` | The amount to increment or decrement the value by. |
| 

value

 | `string` | - | `undefined` | The controlled quantity value. |

## QuantityControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## QuantityInput

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [input attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attributes) . |

## Interfaces

---

### QuantityValueChangeDetail

-   `value: string`
-   `valueAsNumber: number`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity disabled>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Readonly

```jsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity readOnly>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Max

```jsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity max={10}>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Min

```jsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity min={0}>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Step

```jsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity step={10}>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Form field

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Set a quantity:      </FormFieldLabel>
      <Quantity>
        <QuantityControl>
          <QuantityInput />
        </QuantityControl>
      </Quantity>
    </FormField>
}
```

## Recipes

---

Config Tile

VPS 1

4 vCore8 Go RAM100 Go 1 day automated backup Unlimited traffic 200 Mbps

12 months6 monthsNo commitment

From€24.46ex. VAT/monthor €13.19 incl. VAT/month

## React Components/Quantity

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultValue` | `` | No |  | The initial quantity value. Use when you don't need to control the value of the quantity. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `max` | `` | No |  | The maximum quantity that can be selected. |
| `min` | `` | No |  | The minimum quantity that can be selected. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onValueChange` | `` | No |  | Callback fired when the value changes. |
| `readOnly` | `` | No |  | Whether the component is readonly. |
| `required` | `` | No |  | Whether the component is required. |
| `step` | `` | No |  | The amount to increment or decrement the value by. |
| `value` | `` | No |  | The controlled quantity value. |


## Subcomponents


### QuantityControl




### QuantityInput



## Examples


### Accessibility Label

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Number of CPUs:
      </FormFieldLabel>

      <Quantity max={10} min={0}>
        <QuantityControl>
          <QuantityInput />
        </QuantityControl>
      </Quantity>
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Quantity defaultValue="0" min={0}>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Quantity disabled={arg.disabled} invalid={arg.invalid} max={arg.max} min={arg.min} readOnly={arg.readOnly} step={arg.step}>
      <QuantityControl>
        <QuantityInput placeholder={arg.placeholder} />
      </QuantityControl>
    </Quantity>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    max: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'number'
      }
    },
    min: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'number'
      }
    },
    placeholder: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    step: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'number'
      }
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity disabled>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Set a quantity:
      </FormFieldLabel>

      <Quantity>
        <QuantityControl>
          <QuantityInput />
        </QuantityControl>
      </Quantity>
    </FormField>
}
```

### Max

```tsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity max={10}>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Min

```tsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity min={0}>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Quantity defaultValue="0" min={0}>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Readonly

```tsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity readOnly>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Step

```tsx
{
  globals: {
    imports: `import { Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Quantity step={10}>
      <QuantityControl>
        <QuantityInput />
      </QuantityControl>
    </Quantity>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    alignItems: 'flex-start'
  }}>
      <Quantity>
        <QuantityControl>
          <QuantityInput />
        </QuantityControl>
      </Quantity>

      <Quantity disabled>
        <QuantityControl>
          <QuantityInput />
        </QuantityControl>
      </Quantity>

      <Quantity readOnly>
        <QuantityControl>
          <QuantityInput defaultValue="3" />
        </QuantityControl>
      </Quantity>

      <Quantity invalid>
        <QuantityControl>
          <QuantityInput />
        </QuantityControl>
      </Quantity>
    </div>
}
```

## React Components

# Query Filter

## Overview

---

The **Query Filter** component allows users to find specific items within a collection by constructing queries based on properties and property values. Users can combine structured property conditions with free-text search to create precise filters without writing raw query syntax.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>QueryFilter</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=0-1&amp;p=f&amp;t=kwInPIiCg5eOsgBa-0" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/query-filter" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Query Filter** is typically used as a toolbar or input control above data tables or card grids to help users quickly narrow down large datasets.

It is commonly used in:

-   Dashboards and admin interfaces.
-   Productivity tools with lists or tables of items.
-   Applications where filtering and search are core workflows.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Query Filter to enable multi-property search and filtering |
| - Apply the Query Filter to the full dataset, not just currently visible or loaded items |
| - Keep the query readable by ordering tokens logically and avoiding unnecessary duplication of conditions |

| ❌ Don't |
| --- |
| - Don't rely on Query Filter for very small collections where scanning the list is faster |
| - Don't use property tokens for datasets dominated by long free-form text |
| - Don't use operators that don't match the property type (e.g., "between" for single-value properties) |

### Best Practices in Context

1.  **QueryFilter**
2.  **Filter input**
3.  **Suggestions panel**
4.  **Token**
5.  **Clear all filters button**

## Placement

---

The **Query Filter** component is typically positioned above the target data container.

## Behavior

---

### Opening & Closing

Clicking the filter input opens the suggestions panel.

The panel closes when:

-   A value is selected.
-   Clicking outside the component.
-   The Escape key is pressed.

### Adding a Filter

Suggestions guide the user through:

-   Property selection
-   Operator selection
-   Value entry

Free-text is allowed and creates a distinct token.

### Removing a token

Each token can be removed by click.

Backspace behavior does not remove last token unless specifically focused on it.

Optional "Clear all filters" button removes all tokens in one action.

## Navigation

---

### Focus Management

Focus follows a logical order from left to right.

When the suggestions panel is open, focus moves within the panel without leaving the component.

### General Keyboard Shortcuts

Pressing Tab moves focus between the input field, and other interactive elements.

Pressing Arrow Down opens the suggestions panel and moves focus to the first suggestion.

Pressing Escape closes the suggestions panel.

Pressing Backspace deletes characters in the input field

## Accessibility

---

Use ARIA (Accessible Rich Internet Applications) live regions to announce filtering changes. For example:

-   When a filter is added: `aria-live` region could say "Filter 'Status: Active' has been applied.".
-   When a filter is removed: `aria-live` region could say "Filter 'Status: Active' has been removed.".

## React Components

# Query Filter

## Overview

---

## Anatomy

---

QueryFilter

QueryFilterClear

QueryFilterContent

QueryFilterControl

QueryFilterTags

---

## QueryFilter

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
allowCustomValue

 | `boolean` | - | `undefined` | Whether to allow adding a custom filter. |
| 

defaultOpen

 | `boolean` | - | `undefined` | The initial open state of the query filter. Use when you don't need to control the open state of the query filter. |
| 

defaultValue

 | `string[][]` | - | `undefined` | The initial selected value(s). Use when you don't need to control the selected value(s) of the query filter. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

filterOption

 | `Record` |  | `undefined` | The property filter options (operator and value). |
| 

filterProperty

 | `object` |  | `undefined` | The properties that may be used as a filter. |
| 

label

 | `string` |  | `-` | - |
| 

options

 | `Array` |  | `-` | - |
| 

highlightResults

 | `boolean` | - | `false` | Whether to highlight the matching part of filtered items. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override (see Input i18n keys). |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

newElementLabel

 | `string` | - | `'Use: '` | Label displayed in front of a custom filter to add. |
| 

noResultLabel

 | `string` | - | `'No results found'` | Label displayed when no values match the current input value. |
| 

onInputValueChange

 | `(value: QueryFilterInputValueChangeDetails) => void` | - | `undefined` | Callback fired when the input value changes. |
| 

onOpenChange

 | `(detail: QueryFilterOpenChangeDetail) => void` | - | `undefined` | Callback fired when the query filter open state changes. |
| 

onValueChange

 | `(value: QueryFilterValueChangeDetails) => void` | - | `undefined` | Callback fired when the value(s) changes. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the query filter. |
| 

overlayConfig

 | `object` | - | `undefined` | The overlay configuration. |
| 

flip

 | `boolean` | - | `-` | Whether to flip the position. |
| 

sameWidth

 | `boolean` | - | `-` | Whether to make the floating element same width as the reference element. |
| 

readOnly

 | `boolean` | - | `undefined` | Whether the component is readonly. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. |
| 

value

 | `string[][]` | - | `undefined` | The controlled selected value(s). |

## QueryFilterClear

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) . |
| 
color

 | `BUTTON_COLOR` | - | `undefined` | The color preset to use. |
| 

loading

 | `boolean` | - | `undefined` | Whether the component is in loading state, disabling it. |
| 

size

 | `BUTTON_SIZE` | - | `undefined` | The size preset to use. |
| 

variant

 | `BUTTON_VARIANT` | - | `undefined` | The variant preset to use. |

## QueryFilterContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. |

## QueryFilterControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) . |
| 
clearable

 | `boolean` | - | `undefined` | Whether the clear button is displayed. |
| 

loading

 | `boolean` | - | `undefined` | Whether the component is in loading state. |
| 

placeholder

 | `string` | - | `undefined` | The placeholder text to display in the input. |

## QueryFilterTags

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [ul attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul) . |

## Interfaces

---

### QueryFilterInputValueChangeDetails

-   `inputValue: string`

### QueryFilterItem

-   `label: string`
-   `value: string`

### QueryFilterOpenChangeDetail

-   `open: boolean`

### QueryFilterOption

-   `Record<string>`

### QueryFilterProperty

-   `QueryFilterGroup`

### QueryFilterValueChangeDetails

-   `value: undefined[]`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-query-filter-control-column-gap | calc(var(--ods-theme-column-gap) / 2) | 
 |
| --ods-query-filter-control-padding-vertical | calc(var(--ods-theme-input-padding-vertical) / 2) | 

 |
| --ods-query-filter-control-row-gap | calc(var(--ods-theme-row-gap) / 2) | 

 |
| --ods-query-filter-group-option-padding-horizontal | calc(var(--ods-theme-input-padding-horizontal) * 3) | 

 |

## Examples

---

### Default

```jsx
const filterProperty = useMemo(() => ({
    label: 'Properties',
    options: [{
      label: 'Instance ID',
      value: 'instance-id'
    }, {
      label: 'States',
      value: 'states'
    }]
  }), []);
  const filterOption = useMemo(() => ({
    ['instance-id']: {
      operator: {
        label: 'Operators I',
        options: [{
          label: 'equals',
          value: '='
        }, {
          label: 'does not equal',
          value: '!='
        }]
      },
      value: {
        label: 'Instance Values',
        options: [{
          label: 'instance 1',
          value: 'instance-1'
        }, {
          label: 'instance 2',
          value: 'instance-2'
        }]
      }
    },
    states: {
      operator: {
        label: 'Operators S',
        options: [{
          label: 'equals',
          value: '='
        }, {
          label: 'does not equal',
          value: '!='
        }]
      },
      value: {
        label: 'State Values',
        options: [{
          label: 'Running',
          value: 'running'
        }, {
          label: 'Stopped',
          value: 'stopped'
        }]
      }
    }
  }), []);
  return <QueryFilter filterOption={filterOption} filterProperty={filterProperty}>
      <QueryFilterControl />
      <QueryFilterContent />
      <QueryFilterTags />
      <QueryFilterClear>
        Clear all      </QueryFilterClear>
    </QueryFilter>;
}
```

## Recipes

---

Data Grid With Query Filter

| 
Instance ID

 | 

Location

 | 

Model

 | 

Image

 | 

Backup Logic

 | 

Running since

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

## React Components/Query Filter

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `allowCustomValue` | `` | No |  | Whether to allow adding a custom filter. |
| `defaultOpen` | `` | No |  | The initial open state of the query filter. Use when you don't need to control the open state of the query filter. |
| `defaultValue` | `` | No |  | The initial selected value(s). Use when you don't need to control the selected value(s) of the query filter. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `filterOption` | `` | Yes |  | The property filter options (operator and value). |
| `filterProperty` | `` | Yes |  | The properties that may be used as a filter. |
| `highlightResults` | `` | No | false | Whether to highlight the matching part of filtered items. |
| `i18n` | `` | No |  | Internal translations override (see Input i18n keys). |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `newElementLabel` | `` | No | 'Use: ' | Label displayed in front of a custom filter to add. |
| `noResultLabel` | `` | No | 'No results found' | Label displayed when no values match the current input value. |
| `onInputValueChange` | `` | No |  | Callback fired when the input value changes. |
| `onOpenChange` | `` | No |  | Callback fired when the query filter open state changes. |
| `onValueChange` | `` | No |  | Callback fired when the value(s) changes. |
| `open` | `` | No |  | The controlled open state of the query filter. |
| `overlayConfig` | `` | No |  | The overlay configuration. |
| `readOnly` | `` | No |  | Whether the component is readonly. |
| `required` | `` | No |  | Whether the component is required. |
| `value` | `` | No |  | The controlled selected value(s). |


## Subcomponents


### QueryFilterClear



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No |  | @type=BUTTON_COLOR The color preset to use. |
| `loading` | `` | No |  | Whether the component is in loading state, disabling it. |
| `size` | `` | No |  | The size preset to use. |
| `variant` | `` | No |  | The variant preset to use. |



### QueryFilterContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. |



### QueryFilterControl



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `clearable` | `` | No |  | Whether the clear button is displayed. |
| `loading` | `` | No |  | Whether the component is in loading state. |
| `placeholder` | `` | No |  | The placeholder text to display in the input. |



### QueryFilterTags



## Examples


### Anatomy Tech

```tsx
{
  parameters: {
    layout: 'start'
  },
  tags: ['!dev'],
  render: ({}) => <QueryFilter filterOption={filterOption} filterProperty={filterProperty} open overlayConfig={{
    flip: false
  }} value={[['instance-id', '!=', 'instance-1'], ['states', '===', 'running']]}>
      <QueryFilterControl />

      <QueryFilterContent createPortal={false} />

      <div style={{
      display: 'flex',
      flexDirection: 'row',
      gap: '8px',
      alignItems: 'center',
      marginTop: '100px'
    }}>
        <QueryFilterTags style={{
        justifyContent: 'end'
      }} />

        <QueryFilterClear>
          Clear
        </QueryFilterClear>
      </div>
    </QueryFilter>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { QueryFilter, QueryFilterClear, QueryFilterControl, QueryFilterContent, QueryFilterTags } from '@ovhcloud/ods-react';
import { useMemo } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const filterProperty = useMemo(() => ({
      label: 'Properties',
      options: [{
        label: 'Instance ID',
        value: 'instance-id'
      }, {
        label: 'States',
        value: 'states'
      }]
    }), []);
    const filterOption = useMemo(() => ({
      ['instance-id']: {
        operator: {
          label: 'Operators I',
          options: [{
            label: 'equals',
            value: '='
          }, {
            label: 'does not equal',
            value: '!='
          }]
        },
        value: {
          label: 'Instance Values',
          options: [{
            label: 'instance 1',
            value: 'instance-1'
          }, {
            label: 'instance 2',
            value: 'instance-2'
          }]
        }
      },
      states: {
        operator: {
          label: 'Operators S',
          options: [{
            label: 'equals',
            value: '='
          }, {
            label: 'does not equal',
            value: '!='
          }]
        },
        value: {
          label: 'State Values',
          options: [{
            label: 'Running',
            value: 'running'
          }, {
            label: 'Stopped',
            value: 'stopped'
          }]
        }
      }
    }), []);
    return <QueryFilter filterOption={filterOption} filterProperty={filterProperty}>
        <QueryFilterControl />

        <QueryFilterContent />

        <QueryFilterTags />

        <QueryFilterClear>
          Clear all
        </QueryFilterClear>
      </QueryFilter>;
  }
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <QueryFilter allowCustomValue={arg.allowCustomValue} disabled={arg.disabled} highlightResults={arg.highlightResults} invalid={arg.invalid} filterOption={filterOption} filterProperty={filterProperty} newElementLabel={arg.newElementLabel} noResultLabel={arg.noResultLabel} readOnly={arg.readOnly}>
      <QueryFilterControl clearable={arg.clearable} loading={arg.loading} placeholder={arg.placeholder} />

      <QueryFilterContent />

      <QueryFilterTags />

      <QueryFilterClear>
        {arg.clearAllLabel}
      </QueryFilterClear>
    </QueryFilter>,
  argTypes: orderControls({
    allowCustomValue: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    clearAllLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    clearable: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    highlightResults: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    loading: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    newElementLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    noResultLabel: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    placeholder: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    }
  }),
  args: {
    clearAllLabel: 'Clear all',
    placeholder: 'Add filters'
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <QueryFilter filterOption={filterOption} filterProperty={filterProperty}>
      <QueryFilterControl style={{
      alignSelf: 'end',
      width: '300px'
    }} />

      <QueryFilterContent />

      <div style={{
      display: 'flex',
      flexDirection: 'row',
      gap: '8px',
      alignItems: 'center'
    }}>
        <QueryFilterTags style={{
        justifyContent: 'end'
      }} />

        <QueryFilterClear>
          Clear
        </QueryFilterClear>
      </div>
    </QueryFilter>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <QueryFilter filterOption={filterOption} filterProperty={filterProperty}>
      <QueryFilterControl />

      <QueryFilterContent />

      <QueryFilterTags />

      <QueryFilterClear>
        Clear all
      </QueryFilterClear>
    </QueryFilter>
}
```

## React Components

# Radio Group

_A **Radio** button allows users to select only one option from a number of choices._

HTML

CSS

JavaScript

## Overview

---

**Radio Group** is a group of Radios to select a single state from multiples options.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Radio</td></tr><tr><th scope="row">Also known as</th><td>Radio button (previous name)</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=49-14625" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/radio-group" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-radio--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

In most situations where you want to present a list of mutually exclusive options.

**Radio groups** can be used within a form.

Also, it can serve as :

-   Selecting choice
-   Lists/sub-lists
-   Filters

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Radio Group when users must choose only one option from a set of two or more |
| - Ensure each radio has a clear and concise label, wrapped text is preferable to truncated labels |
| - Keep the number of radios concise (ideally 2 to 5) to avoid overwhelming the user |
| - Consider using a Toggle instead of a two-option Radio Group when the options are affirmative/negative (e.g., "Enable notifications" yes/no) |
| - Group related radios together with a visible label or legend to provide context |

| ❌ Don't |
| --- |
| - Don't use Radio Group when multiple selections are allowed. Use Checkbox instead |
| - Don't use a Radio Group with more than 5 options. Switch to a Select component for better usability |
| - Avoid situations where all options in a set should be deselected |
| - Don't truncate long labels, allow wrapping to maintain readability |

### Best Practices in Context

1.  **Radio Group**
2.  **Radio button**
3.  **Label**

## Placement

---

**Radio Groups** should be vertical, meaning one radio button under another.

Usage of horizontal **Radio Groups** should only occur if vertical space is limited.

## Behavior

---

Radio button can be hovered, focused, clicked or disabled. When disabled, radio button cannot be focused nor clicked.

When clicking on one of the radio button or its linked label, the radio button is selected or deselected depending on the previous state.

Radio button behavior does work only when used in a situation of a **Radio group**. The **Radio group** can be in an error state, but also in a disabled state. It also can be focused and hovered.

## Variation

---

Radio buttons follow the native behavior of the browser used, so the appearance of radio buttons may vary depending on the browser.

## Navigation

---

### Focus Management

When the **Radio Group** component is focused, focus is automatically set to the first focusable and not disabled radio in the group.

If a radio button is checked, focus moves to that checked item when the component is focused.

A read-only radio button can receive focus but cannot be selected or modified.

If a radio button is disabled, it cannot receive focus and cannot be selected.

Focus moves through the radio buttons sequentially.

### General Keyboard Shortcuts

Pressing Tab moves focus to the next focusable and enabled radio button in the group.

Pressing Shift + Tab moves focus backward through the radio buttons.

Pressing Arrow Down or Arrow Right moves focus and selects the next radio button in the group.

Pressing Arrow Up or Arrow Left moves focus and selects the previous radio button in the group.

Pressing Space when focused on an unchecked radio button selects it.

## Accessibility

---

This component complies with the [Radio Group WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/)

Try to keep a vertical spacing of at least `8px` between each radio buttons, to provide sufficient tactile and visual separation.

## React Components

# Radio Group

## Overview

---

## Anatomy

---

RadioGroup

Radio

RadioControl

RadioLabel

---

## RadioGroup

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultValue

 | `string` | - | `undefined` | The initial value of the checked radio. Use when you don't need to control the value of the radio group. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onValueChange

 | `(detail: RadioValueChangeDetail) => void` | - | `undefined` | Callback fired when the value changes. |
| 

orientation

 | `'horizontal' | 'vertical'` | - | `undefined` | The orientation of the radio group. |
| 

value

 | `string` | - | `undefined` | The controlled value of the radio group. |

## Radio

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [label attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/label#attributes) . |
| 
disabled

 | `boolean` | - | `undefined` | Whether the radio is disabled. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the radio is in error state. |
| 

required

 | `boolean` | - | `undefined` | Whether the radio is required. |
| 

value

 | `string` |  | `undefined` | The value of the radio. |

## RadioControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## RadioLabel

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |

## Interfaces

---

### RadioValueChangeDetail

-   `value: string | null`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup>
      <Radio value="html">
        <RadioControl />
        <RadioLabel>HTML</RadioLabel>
      </Radio>
      <Radio value="css">
        <RadioControl />
        <RadioLabel>CSS</RadioLabel>
      </Radio>
      <Radio value="js">
        <RadioControl />
        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup disabled>
      <Radio value="html">
        <RadioControl />
        <RadioLabel>HTML</RadioLabel>
      </Radio>
      <Radio value="css">
        <RadioControl />
        <RadioLabel>CSS</RadioLabel>
      </Radio>
      <Radio value="js">
        <RadioControl />
        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

```jsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup>
      <Radio value="html">
        <RadioControl />
        <RadioLabel>HTML</RadioLabel>
      </Radio>
      <Radio disabled value="css">
        <RadioControl />
        <RadioLabel>CSS</RadioLabel>
      </Radio>
      <Radio value="js">
        <RadioControl />
        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Invalid

```jsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup defaultValue="html">
      <Radio value="html" invalid>
        <RadioControl />
        <RadioLabel>HTML</RadioLabel>
      </Radio>
      <Radio invalid value="css">
        <RadioControl />
        <RadioLabel>CSS</RadioLabel>
      </Radio>
      <Radio value="js">
        <RadioControl />
        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Orientation

HTML

CSS

JavaScript

```jsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup orientation="horizontal">
      <Radio value="html">
        <RadioControl />
        <RadioLabel>HTML</RadioLabel>
      </Radio>
      <Radio value="css">
        <RadioControl />
        <RadioLabel>CSS</RadioLabel>
      </Radio>
      <Radio value="js">
        <RadioControl />
        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Form field

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Pick a language:      </FormFieldLabel>
      <RadioGroup>
        <Radio value="html">
          <RadioControl />
          <RadioLabel>HTML</RadioLabel>
        </Radio>
        <Radio value="css">
          <RadioControl />
          <RadioLabel>CSS</RadioLabel>
        </Radio>
        <Radio value="js">
          <RadioControl />
          <RadioLabel>JavaScript</RadioLabel>
        </Radio>
      </RadioGroup>
    </FormField>
}
```

## Recipes

---

Config Tile

VPS 1

4 vCore8 Go RAM100 Go 1 day automated backup Unlimited traffic 200 Mbps

12 months6 monthsNo commitment

From€24.46ex. VAT/monthor €13.19 incl. VAT/month

## React Components/Radio Group

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultValue` | `` | No |  | The initial value of the checked radio. Use when you don't need to control the value of the radio group. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onValueChange` | `` | No |  | Callback fired when the value changes. |
| `orientation` | `` | No |  | The orientation of the radio group. |
| `value` | `` | No |  | The controlled value of the radio group. |


## Subcomponents


### Radio



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `disabled` | `` | No |  | Whether the radio is disabled. |
| `invalid` | `` | No |  | Whether the radio is in error state. |
| `required` | `` | No |  | Whether the radio is required. |
| `value` | `` | Yes |  | The value of the radio. |



### RadioControl




### RadioLabel



## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <RadioGroup>
      <Radio value="html">
        <RadioControl />

        <RadioLabel>HTML</RadioLabel>
      </Radio>

      <Radio value="css">
        <RadioControl />

        <RadioLabel>CSS</RadioLabel>
      </Radio>

      <Radio value="js">
        <RadioControl />

        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup>
      <Radio value="html">
        <RadioControl />

        <RadioLabel>HTML</RadioLabel>
      </Radio>

      <Radio value="css">
        <RadioControl />

        <RadioLabel>CSS</RadioLabel>
      </Radio>

      <Radio value="js">
        <RadioControl />

        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <RadioGroup disabled={arg.disabled} orientation={arg.orientation}>
      <Radio invalid={arg.invalid} value="html">
        <RadioControl />

        <RadioLabel>HTML</RadioLabel>
      </Radio>

      <Radio invalid={arg.invalid} value="css">
        <RadioControl />

        <RadioLabel>CSS</RadioLabel>
      </Radio>

      <Radio invalid={arg.invalid} value="js">
        <RadioControl />

        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    orientation: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: ['horizontal', 'vertical']
        }
      },
      control: {
        type: 'select'
      },
      options: ['horizontal', 'vertical']
    }
  })
}
```

### Disabled Group

```tsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup disabled>
      <Radio value="html">
        <RadioControl />

        <RadioLabel>HTML</RadioLabel>
      </Radio>

      <Radio value="css">
        <RadioControl />

        <RadioLabel>CSS</RadioLabel>
      </Radio>

      <Radio value="js">
        <RadioControl />

        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Disabled Item

```tsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup>
      <Radio value="html">
        <RadioControl />

        <RadioLabel>HTML</RadioLabel>
      </Radio>

      <Radio disabled value="css">
        <RadioControl />

        <RadioLabel>CSS</RadioLabel>
      </Radio>

      <Radio value="js">
        <RadioControl />

        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Pick a language:
      </FormFieldLabel>

      <RadioGroup>
        <Radio value="html">
          <RadioControl />

          <RadioLabel>HTML</RadioLabel>
        </Radio>

        <Radio value="css">
          <RadioControl />

          <RadioLabel>CSS</RadioLabel>
        </Radio>

        <Radio value="js">
          <RadioControl />

          <RadioLabel>JavaScript</RadioLabel>
        </Radio>
      </RadioGroup>
    </FormField>
}
```

### Invalid

```tsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup defaultValue="html">
      <Radio value="html" invalid>
        <RadioControl />

        <RadioLabel>HTML</RadioLabel>
      </Radio>

      <Radio invalid value="css">
        <RadioControl />

        <RadioLabel>CSS</RadioLabel>
      </Radio>

      <Radio value="js">
        <RadioControl />

        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Orientation

```tsx
{
  globals: {
    imports: `import { Radio, RadioControl, RadioGroup, RadioLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <RadioGroup orientation="horizontal">
      <Radio value="html">
        <RadioControl />

        <RadioLabel>HTML</RadioLabel>
      </Radio>

      <Radio value="css">
        <RadioControl />

        <RadioLabel>CSS</RadioLabel>
      </Radio>

      <Radio value="js">
        <RadioControl />

        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <RadioGroup>
      <Radio value="html">
        <RadioControl />

        <RadioLabel>HTML</RadioLabel>
      </Radio>

      <Radio value="css">
        <RadioControl />

        <RadioLabel>CSS</RadioLabel>
      </Radio>

      <Radio value="js">
        <RadioControl />

        <RadioLabel>JavaScript</RadioLabel>
      </Radio>
    </RadioGroup>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    gap: '24px'
  }}>
      <RadioGroup>
        <Radio value="html"><RadioControl /><RadioLabel>HTML</RadioLabel></Radio>
        <Radio value="css"><RadioControl /><RadioLabel>CSS</RadioLabel></Radio>
        <Radio value="js"><RadioControl /><RadioLabel>JavaScript</RadioLabel></Radio>
      </RadioGroup>

      <RadioGroup disabled>
        <Radio value="html"><RadioControl /><RadioLabel>HTML</RadioLabel></Radio>
        <Radio value="css"><RadioControl /><RadioLabel>CSS</RadioLabel></Radio>
        <Radio value="js"><RadioControl /><RadioLabel>JavaScript</RadioLabel></Radio>
      </RadioGroup>

      <RadioGroup>
        <Radio disabled value="html"><RadioControl /><RadioLabel>HTML</RadioLabel></Radio>
        <Radio value="css"><RadioControl /><RadioLabel>CSS</RadioLabel></Radio>
        <Radio value="js"><RadioControl /><RadioLabel>JavaScript</RadioLabel></Radio>
      </RadioGroup>
    </div>
}
```

## React Components

# Range

_**Range** component is used to allow users to visually select one or more values from a range of values by moving the handle along a bar_

0100

## Overview

---

The **Range** component is used for selecting a numerical value or a range of values within a specified range.

It provides a visual and interactive way to adjust values, often used in forms, settings, and data filtering.

The component can support single-value sliders or dual-handle sliders for selecting ranges.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Range</td></tr><tr><th scope="row">Also known as</th><td>Slider</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=49-16052" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/range" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-range--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

A **Range** is used to select a numeric value within a given range with defined minimum and maximum values.

It is easily adjustable in a visually pleasing interface and, after interacting with it, changes are reflected immediately.

A dual **Range** allows the user to select a numeric range value that is no less than a given value, and no more than another given value.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Range component when users need to select a value (or values) within a defined numeric interval |
| - Label the Range clearly using a Form Field to indicate what the value represents |
| - Provide a default value or starting range to guide users and avoid ambiguity |
| - Use a dual Range when allowing users to set a minimum and maximum value |
| - Make sure the Range has enough horizontal space to be usable and legible |

| ❌ Don't |
| --- |
| - Don't overload a page with too many Range components |
| - Don't place a Range in tight or constrained spaces where the control becomes hard to use or interpret |
| - Don't use a continuous Range for large intervals where precision is important. Use a discrete step Range or typed input instead |
| - Don't omit labels or helper text |

### Best Practices in Context

1.  **Range**
2.  **Track**
3.  **Min/Max values**
4.  **Handle**
5.  **Active fill**
6.  **Second handle** - optional (Dual Range)
7.  **Tick marks** - optional

## Placement

---

A **Range** can be used in a page as long as there is a need to allow users to select a numeric value within a given range, for a quantity or a volume for example.

It has a width by default, but it can widen to match its container.

The **Range** can also be used inside components.

It also can be combined with other elements such as an input for a better control of the value selection.

## Behavior

---

A **Range** can be focused and hovered. It also can be disabled.

The user can select a numeric value by clicking and dragging the thumb along the track.

## Navigation

---

### Focus Management

The **Range** component can be focused. Focus will land on the first thumb if multiple are present.

When multiple thumbs are used, each thumb can receive focus individually.

Disabled **Range** and disabled thumbs cannot be focused or interacted with.

### General Keyboard Shortcuts

Pressing Arrow Right or Arrow Up increases the value of the focused thumb by one step.

Pressing Arrow Left or Arrow Down decreases the value of the focused thumb by one step.

Pressing Page Up or Shift + Arrow Right increases the value by a larger step.

Pressing Page Down or Shift + Arrow Down decreases the value by a larger step.

Pressing Home or fn + Arrow Left sets the value to the minimum.

Pressing End or fn + Arrow Right sets the value to the maximum.

Pressing Tab and Shift + Tab allow navigation between multiple thumbs (if present).

## Accessibility

---

This component complies with the [Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) and [Slider (Multi-Thumb)](https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/) WAI-ARIA design patterns.

### Always provide an explicit label

Every **Range** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Volume

0100

```jsx
<FormField>
  <FormFieldLabel>
    Volume  </FormFieldLabel>
  <Range
    defaultValue={[
      50
    ]}
   />
</FormField>
```

Screen readers will announce the value, the slider and its label.

### Always provide a descriptive sub-label in Dual Range

Sub-label provide additional context about the values that are selected or adjusted by the user. This sub-label should be vocalized by screen readers. Use [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby) to link the sub-label and [aria-live="polite"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions) to ensure that the content is vocalized by screen readers.

Price rangeSelected values: 30 - 70€

0100

```jsx
const [values, setValues] = useState([30, 70]);
  return <FormField>
      <FormFieldLabel id="range-label">
        Price range      </FormFieldLabel>
      <Text aria-live="polite" id="range-sublabel" preset={TEXT_PRESET.caption}>
        Selected values: {values[0]} - {values[1]}€
      </Text>
      <Range aria-labelledby={['range-label', 'range-sublabel']} onDragging={({
      value    }) => setValues(value)} value={values} />
    </FormField>;
}
```

Screen readers will announce the values, the slider and its label.

## React Components

# Range

## Overview

---

## Anatomy

---

Range

---

## Range

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
aria-label

 | `string[]` | - | `undefined` | The aria-label of each slider thumb. Useful for providing an accessible name to the slider. |
| 

aria-labelledby

 | `string[]` | - | `undefined` | The id of the elements that labels each slider thumb. Useful for providing an accessible name to the slider. |
| 

defaultValue

 | `number[]` | - | `undefined` | The initial selected value(s). Use when you don't need to control the value(s) of the range. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

displayBounds

 | `boolean` | - | `undefined` | Whether the range bounds are displayed under the track. |
| 

displayTooltip

 | `boolean` | - | `undefined` | Whether a tooltip with the current thumb value is displayed on drag. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

max

 | `number` | - | `undefined` | The maximum value that can be selected. |
| 

min

 | `number` | - | `undefined` | The minimum value that can be selected. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onDragging

 | `(detail: RangeValueChangeDetail) => void` | - | `undefined` | Callback fired when the thumb moves. |
| 

onValueChange

 | `(detail: RangeValueChangeDetail) => void` | - | `undefined` | Callback fired when the thumb is released. |
| 

step

 | `number` | - | `undefined` | The amount to increment or decrement the value by. |
| 

ticks

 | `RangeTickItem[]` | - | `undefined` | List of tick indicators to display alongside the range. |
| 

value

 | `number[]` | - | `undefined` | The controlled selected value(s). |

## Interfaces

---

### RangeTickCustomItem

-   `label: string`
-   `value: number`

### RangeValueChangeDetail

-   `value: number[]`

## Unions

---

-   `RangeTickItem = number | RangeTickCustomItem`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-range-thumb-background-color | var(--ods-color-neutral-000) | 
 |
| --ods-range-track-border-radius | calc(var(--ods-theme-border-radius) / 2) | 

 |

## Examples

---

### Default

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Range />
      <Range defaultValue={[50, 75]} />
    </>
}
```

### Disabled

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Range defaultValue={[20]} disabled />
      <Range defaultValue={[50, 75]} disabled />
    </>
}
```

### Min / Max

```jsx
{
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Max 500</p>
      <Range defaultValue={[50]} max={500} />
      <Range defaultValue={[50, 75]} max={500} />
      <p>Min 25</p>
      <Range defaultValue={[50]} min={25} />
      <Range defaultValue={[50, 75]} min={25} />
      <p>Max 75 & Min 25</p>
      <Range defaultValue={[50]} max={75} min={25} />
      <Range defaultValue={[50, 75]} max={75} min={25} />
    </>
}
```

### Step

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Range defaultValue={[20]} step={5} />
      <Range defaultValue={[50, 75]} step={5} />
    </>
}
```

### Ticks

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Range defaultValue={[20]} ticks={[10, 20, 30, 40, 50, 60, 70, 80, 90]} />
      <Range defaultValue={[50, 75]} ticks={[10, 20, 30, 40, 50, 60, 70, 80, 90]} />
    </>
}
```

### Custom Ticks

```jsx
<>
  <Range ticks={[{
  label: 'Low',
  value: 25
}, {
  label: 'Medium',
  value: 50
}, {
  label: 'High',
  value: 75
}]} />
  <Range displayBounds={false} displayTooltip={false} max={5} min={1} ticks={[{
  label: 'Very Poor',
  value: 1
}, {
  label: 'Poor',
  value: 2
}, {
  label: 'Average',
  value: 3
}, {
  label: 'Good',
  value: 4
}, {
  label: 'Excellent',
  value: 5
}]} />
</>
```

### Controlled

This is a controlled component. The final value is only updated when the user releases the mouse button.

```jsx
const [draggingValue, setDraggingValue] = useState<number>();
  const [value, setValue] = useState<number>();
  function onDragging(detail: RangeValueChangeDetail) {
    setDraggingValue(detail.value[0]);
  }
  function onValueChange(detail: RangeValueChangeDetail) {
    setValue(detail.value[0]);
  }
  return <>
      <p>
        <span>Final value: {value}</span>
        <br />
        <span>Dragged value: {draggingValue}</span>
      </p>
      <Range onDragging={onDragging} onValueChange={onValueChange} value={draggingValue ? [draggingValue] : undefined} />
    </>;
}
```

### Form field

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { FormField, FormFieldLabel, Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Range:      </FormFieldLabel>
      <Range />
    </FormField>
}
```

## Recipes

---

Range Input

0100

## React Components/Range

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `aria-label` | `` | No |  | The aria-label of each slider thumb. Useful for providing an accessible name to the slider. |
| `aria-labelledby` | `` | No |  | The id of the elements that labels each slider thumb. Useful for providing an accessible name to the slider. |
| `defaultValue` | `` | No |  | The initial selected value(s). Use when you don't need to control the value(s) of the range. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `displayBounds` | `` | No |  | Whether the range bounds are displayed under the track. |
| `displayTooltip` | `` | No |  | Whether a tooltip with the current thumb value is displayed on drag. |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `max` | `` | No |  | The maximum value that can be selected. |
| `min` | `` | No |  | The minimum value that can be selected. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onDragging` | `` | No |  | Callback fired when the thumb moves. |
| `onValueChange` | `` | No |  | Callback fired when the thumb is released. |
| `step` | `` | No |  | The amount to increment or decrement the value by. |
| `ticks` | `` | No |  | List of tick indicators to display alongside the range. |
| `value` | `` | No |  | The controlled selected value(s). |


## Examples


### Accessibility Descriptive Sub Label

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, FormField, FormFieldLabel, Range, Text } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const [values, setValues] = useState([30, 70]);
    return <FormField>
        <FormFieldLabel id="range-label">
          Price range
        </FormFieldLabel>

        <Text aria-live="polite" id="range-sublabel" preset={TEXT_PRESET.caption}>
          Selected values: {values[0]} - {values[1]}€
        </Text>

        <Range aria-labelledby={['range-label', 'range-sublabel']} onDragging={({
        value
      }) => setValues(value)} value={values} />
      </FormField>;
  }
}
```

### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Volume
      </FormFieldLabel>

      <Range defaultValue={[50]} />
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  decorators: [story => <div style={{
    width: '160px'
  }}>{story()}</div>],
  tags: ['!dev'],
  render: ({}) => <Range defaultValue={[50]} />
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [draggingValue, setDraggingValue] = useState<number>();
    const [value, setValue] = useState<number>();
    function onDragging(detail: RangeValueChangeDetail) {
      setDraggingValue(detail.value[0]);
    }
    function onValueChange(detail: RangeValueChangeDetail) {
      setValue(detail.value[0]);
    }
    return <>
        <p>
          <span>Final value: {value}</span>
          <br />
          <span>Dragged value: {draggingValue}</span>
        </p>

        <Range onDragging={onDragging} onValueChange={onValueChange} value={draggingValue ? [draggingValue] : undefined} />
      </>;
  }
}
```

### Default

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Range />

      <Range defaultValue={[50, 75]} />
    </>
}
```

### Demo

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    justifyContent: 'center',
    height: '80vh'
  }}>{story()}</div>],
  render: ({
    dualRange,
    ...arg
  }: DemoArg) => {
    const MAX_VALUE = 100;
    const [values, setValues] = useState([0]);
    useEffect(() => {
      if (dualRange) {
        const step = arg.step || 1;
        const newValue = values[0] === MAX_VALUE ? values[0] - step : values[0];
        setValues([newValue, newValue + step]);
      } else {
        setValues([values[0]]);
      }
    }, [dualRange]);
    return <Range {...arg} max={MAX_VALUE} onDragging={({
      value
    }) => setValues(value)} value={values} />;
  },
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    displayBounds: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    displayTooltip: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    dualRange: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: {
        type: 'boolean'
      }
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    step: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'number'
    }
  })
}
```

### Disabled

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Range defaultValue={[20]} disabled />

      <Range defaultValue={[50, 75]} disabled />
    </>
}
```

### In Form Field

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { FormField, FormFieldLabel, Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Range:
      </FormFieldLabel>

      <Range />
    </FormField>
}
```

### Max Min

```tsx
{
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>Max 500</p>
      <Range defaultValue={[50]} max={500} />
      <Range defaultValue={[50, 75]} max={500} />

      <p>Min 25</p>
      <Range defaultValue={[50]} min={25} />
      <Range defaultValue={[50, 75]} min={25} />

      <p>Max 75 & Min 25</p>
      <Range defaultValue={[50]} max={75} min={25} />
      <Range defaultValue={[50, 75]} max={75} min={25} />
    </>
}
```

### Overview

```tsx
{
  decorators: [story => <div style={{
    width: '160px'
  }}>{story()}</div>],
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Range defaultValue={[50]} />
}
```

### Step

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Range defaultValue={[20]} step={5} />

      <Range defaultValue={[50, 75]} step={5} />
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    width: '240px'
  }}>
      <Range />
      <Range defaultValue={[50, 75]} />
      <Range disabled defaultValue={[20]} />
      <Range disabled defaultValue={[50, 75]} />
      <Range step={5} defaultValue={[20]} />
      <Range ticks={[10, 20, 30, 40, 50, 60, 70, 80, 90]} defaultValue={[20]} />
    </div>
}
```

### Ticks

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Range defaultValue={[20]} ticks={[10, 20, 30, 40, 50, 60, 70, 80, 90]} />

      <Range defaultValue={[50, 75]} ticks={[10, 20, 30, 40, 50, 60, 70, 80, 90]} />
    </>
}
```

### Ticks Labels

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Range } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => <>
      <Range ticks={[{
      label: 'Low',
      value: 25
    }, {
      label: 'Medium',
      value: 50
    }, {
      label: 'High',
      value: 75
    }]} />

      <Range displayBounds={false} displayTooltip={false} max={5} min={1} ticks={[{
      label: 'Very Poor',
      value: 1
    }, {
      label: 'Poor',
      value: 2
    }, {
      label: 'Average',
      value: 3
    }, {
      label: 'Good',
      value: 4
    }, {
      label: 'Excellent',
      value: 5
    }]} />
    </>
}
```

## React Components

# Select

_**Select** component is used to select one or more items from a list of values_

DogCatHamsterParrotSpiderGoldfish

## Overview

---

A **Select** is used to allow users to select one or more items from a list that is displayed after clicking on it.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Select</td></tr><tr><th scope="row">Also known as</th><td>Dropdown</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=49-24314" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/select" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-select--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

A **Select** is used to display to the user a list of items or options to choose from.

The order of items or options in the list should be sorted as following, depending on the usage:

-   **Frequency** of use: recommended item will be listed first
-   **Alpha**: it can be used for countries or city locations for example
-   **Numeric**: it can be used for sizes for example

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Select when users need to choose one option from a list of three or more items |
| - Order the options logically (e.g., by frequency of use, alphabetical order, or numeric sequence) |
| - Keep option labels short and scannable, ideally one word or a short phrase that fits on a single line |
| - Use placeholder text to prompt users when no option is selected |
| - Consider adding grouping if you have many related options and want to improve clarity |

| ❌ Don't |
| --- |
| - Don't use a Select for binary choices like "Yes" or "No". Use Radio buttons instead for faster interaction |
| - Don't use Select when users need to compare all available options. Use Radio buttons for better visibility |
| - Don't include very long option labels that wrap multiple lines since it makes scanning and selection harder |
| - Don't rely only on placeholder text to communicate intent. Always pair Select with a label |
| - Don't overload Select with too many items (e.g., hundreds). Consider Combobox or a searchable alternative |

### Best Practices in Context

1.  **Select**
2.  **Field containing placeholder or selection**
3.  **Dropdown**
4.  **List**
5.  **Option**
6.  **Group** - optional
7.  **Multi-selection** - optional (display a checkbox)

## Placement

---

**Select** should be vertically aligned with other form components on a same page.

It has a width by default, but it can widen to match its container.

## Behavior

---

**Select** can be focused and hovered. It also can be disabled. A single option and a group of options can be disabled to be more precise in inner **Select** items.

## Variation

---

-   **Single selection**: Allows the user to select a single item from a dropdown list of options.
-   **Multiple selection**: Allows the user to select multiple items from a dropdown list of options. The whole line is clickable.
-   **Grouped options**: Related options can be grouped together in both a single and multi **Select**.

## Navigation

---

### Focus Management

When the **Select** is enabled, it can receive focus via keyboard interaction.

When the **Select** is opened, focus moves into the dropdown content and highlights the selected option (if any), or the first available option.

When closed, focus returns to the **Select** trigger.

A read-only **Select** can receive focus, but the dropdown cannot be opened or interacted with.

Disabled **Select** or disabled options cannot receive focus or be interacted with.

### General Keyboard Shortcuts

Pressing Space, Enter, Arrow Down or Arrow Up when the **Select** is focused opens the dropdown and highlights the selected or first option.

While the dropdown is open:

-   Arrow Down moves the highlight to the next option
-   Arrow Up moves the highlight to the previous option
-   Enter or Tab selects the currently highlighted option and closes the dropdown
-   Escape closes the dropdown without making a new selection

Typing an alphanumeric character (A–Z, a–z) changes focus to the next option starting with that character.

## Accessibility

---

This component complies with the [Listbox WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) .

### Always provide an explicit label

Every **Select** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Select a Web hosting

1 vCore 2,4 GHz, 2 Go RAM1 vCore 2,4 GHz, 4 Go RAM2 vCores 2,4 GHz, 8 Go RAM

```jsx
<FormField>
  <FormFieldLabel>
    Select a Web hosting  </FormFieldLabel>
  <Select
    items={[
      {
        label: '1 vCore 2,4 GHz, 2 Go RAM',
        value: 'hosting-1'
      },
      {
        label: '1 vCore 2,4 GHz, 4 Go RAM',
        value: 'hosting-2'
      },
      {
        label: '2 vCores 2,4 GHz, 8 Go RAM',
        value: 'hosting-3'
      }
    ]}
  >
    <SelectControl />
    <SelectContent />
  </Select>
</FormField>
```

Screen readers will announce the label as well as the select.

## React Components

# Select

## Overview

---

## Anatomy

---

Select

SelectContent

SelectControl

---

## Select

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultOpen

 | `boolean` | - | `undefined` | The initial open state of the select. Use when you don't need to control the open state of the select. |
| 

defaultValue

 | `string | string[]` | - | `undefined` | The initial selected value(s). Use when you don't need to control the selected value(s) of the select. |
| 

disabled

 | `boolean` | - | `false` | Whether the component is disabled. |
| 

fitControlWidthDeprecated

 | `boolean` | - | `true` | Use overlayConfig.sameWidth instead |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

items

 | `SelectItem[]` | - | `[]` | The list of items |
| 

multiple

 | `boolean | 'merge'` | - | `false` | Allows multiple selection and define how it should be rendered. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onOpenChange

 | `(detail: SelectOpenChangeDetail) => void` | - | `undefined` | Callback fired when the select open state changes. |
| 

onValueChange

 | `(detail: SelectValueChangeDetail) => void` | - | `undefined` | Callback fired when the value(s) changes. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the select. |
| 

overlayConfig

 | `object` | - | `undefined` | The overlay configuration. |
| 

flip

 | `boolean` | - | `-` | Whether to flip the position. |
| 

sameWidth

 | `boolean` | - | `-` | Whether to make the floating element same width as the reference element. |
| 

positionerStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |
| 

readOnly

 | `boolean` | - | `false` | Whether the component is readonly. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. |
| 

value

 | `string[]` | - | `undefined` | The controlled selected value(s). |

## SelectContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. |
| 

customGroupRenderer

 | `(arg: SelectCustomGroupRendererArg) => JSX.Element` | - | `undefined` | Custom render for each group item. |
| 

customOptionRenderer

 | `(arg: SelectCustomOptionRendererArg) => JSX.Element` | - | `undefined` | Custom render for each option item. |

## SelectControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
customItemRenderer

 | `(arg: SelectCustomItemRendererArg) => JSX.Element` | - | `undefined` | Custom render for the selected item(s). |
| 

multipleSelectionLabel

 | `string` | - | `undefined` | Label displayed on multiple selection when in "merge" mode. |
| 

placeholder

 | `string` | - | `undefined` | The placeholder text to display in the select. |

## Interfaces

---

### SelectCustomGroupRendererArg<T>

-   `customData?: T`
-   `label: string`

### SelectCustomItemRendererArg<T>

-   `selectedItems: SelectOptionItem[]`
-   `text: string`
-   `values: string[]`

### SelectCustomOptionRendererArg<T>

-   `customData?: T`
-   `label: string`

### SelectGroupItem<T>

-   `customRendererData?: T`
-   `disabled?: boolean`
-   `label: string`
-   `options: SelectOptionItem[]`

### SelectOpenChangeDetail

-   `open: boolean`

### SelectOptionItem<T>

-   `customRendererData?: T`
-   `disabled?: boolean`
-   `label: string`
-   `value: string`

### SelectValueChangeDetail

-   `items: SelectItem[]`
-   `value: string[]`

## Unions

---

-   `SelectItem<T> = SelectGroupItem | SelectOptionItem`
-   `SelectMultipleMode = boolean | "merge"`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-select-group-option-padding-horizontal | calc(var(--ods-theme-input-padding-horizontal) * 3) | 
 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Select items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <SelectControl />
      <SelectContent />
    </Select>
}
```

### Group

```jsx
{
  globals: {
    imports: `import { Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Select items={[{
    label: 'Europe',
    options: [{
      label: 'France',
      value: 'fr'
    }, {
      label: 'Germany',
      value: 'de'
    }, {
      label: 'Italy',
      value: 'it'
    }]
  }, {
    label: 'Asia',
    options: [{
      label: 'China',
      value: 'cn'
    }, {
      label: 'Japan',
      value: 'jp'
    }, {
      label: 'Russia',
      value: 'ru'
    }]
  }, {
    label: 'North America',
    options: [{
      label: 'Canada',
      value: 'ca'
    }, {
      label: 'Mexico',
      value: 'mx'
    }, {
      label: 'United States of America',
      value: 'us'
    }]
  }]}>
      <SelectControl />
      <SelectContent />
    </Select>
}
```

### Multiple Selection

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { TEXT_PRESET, Select, SelectContent, SelectControl, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Select items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]} multiple>
        <Text htmlFor="multiple" preset={TEXT_PRESET.label}>
          Default multiple selection        </Text>
        <SelectControl id="multiple" placeholder="Select one or more pets" />
        <SelectContent />
      </Select>
      <Select items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]} multiple="merge">
        <Text htmlFor="multiple-merged" preset={TEXT_PRESET.label}>
          Merged multiple selection        </Text>
        <SelectControl id="multiple-merged" placeholder="Select one or more pets" />
        <SelectContent />
      </Select>
    </>
}
```

### Disabled

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { TEXT_PRESET, Select, SelectContent, SelectControl, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Select disabled items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]}>
        <Text htmlFor="disabled" preset={TEXT_PRESET.label}>
          Disabled        </Text>
        <SelectControl id="disabled" placeholder="Select one or more pets" />
        <SelectContent />
      </Select>
      <Select items={[{
      label: 'Dog',
      value: 'dog',
      disabled: true
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot',
      disabled: true
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]}>
        <Text htmlFor="disabled-options" preset={TEXT_PRESET.label}>
          Disabled options        </Text>
        <SelectControl id="disabled-options" placeholder="Select one or more pets" />
        <SelectContent />
      </Select>
      <Select items={[{
      label: 'Europe',
      options: [{
        label: 'France',
        value: 'fr'
      }, {
        label: 'Germany',
        value: 'de',
        disabled: true
      }, {
        label: 'Italy',
        value: 'it'
      }]
    }, {
      disabled: true,
      label: 'Asia',
      options: [{
        label: 'China',
        value: 'cn',
        disabled: true
      }, {
        label: 'Japan',
        value: 'jp',
        disabled: true
      }, {
        label: 'Russia',
        value: 'ru',
        disabled: true
      }]
    }, {
      label: 'North America',
      options: [{
        label: 'Canada',
        value: 'ca'
      }, {
        label: 'Mexico',
        value: 'mx'
      }, {
        label: 'United States of America',
        value: 'us'
      }]
    }]}>
        <Text htmlFor="disabled-group" preset={TEXT_PRESET.label}>
          Disabled group or group option        </Text>
        <SelectControl id="disabled-group" />
        <SelectContent />
      </Select>
    </>
}
```

### Readonly

```jsx
{
  globals: {
    imports: `import { Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Select items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]} readOnly>
      <SelectControl placeholder="Select one or more pets" />
      <SelectContent />
    </Select>
}
```

### Custom Renderer

```jsx
type CustomData = {
    description?: string;
    flag?: string;
    logo?: string;
  };
  const items: SelectItem<CustomData>[] = [{
    customRendererData: {
      flag: 'https://upload.wikimedia.org/wikipedia/commons/b/b7/Flag_of_Europe.svg'
    },
    label: 'EU providers',
    options: [{
      customRendererData: {
        description: 'OVH, legally OVH Groupe SA, is a French cloud computing company which offers VPS, dedicated servers and other web services. As of 2016 OVH owned the world\'s largest data center in surface area. As of 2019, it was the largest hosting provider in Europe, and the third largest in the world based on physical servers.',
        logo: 'https://ovh.github.io/manager/ovhcloud-logo.webp'
      },
      label: 'OVH Cloud',
      value: 'ovh'
    }]
  }, {
    customRendererData: {
      flag: 'https://upload.wikimedia.org/wikipedia/en/a/a4/Flag_of_the_United_States.svg'
    },
    label: 'US providers',
    options: [{
      customRendererData: {
        description: 'Amazon Web Services, Inc. is a subsidiary of Amazon that provides on-demand cloud computing platforms and APIs to individuals, companies, and governments, on a metered, pay-as-you-go basis. Clients will often use this in combination with autoscaling.',
        logo: 'https://cdn.icon-icons.com/icons2/2407/PNG/512/aws_icon_146074.png'
      },
      label: 'Amazon Web Services',
      value: 'aws'
    }, {
      customRendererData: {
        description: 'Microsoft Azure, often referred to as just Azure, is a cloud computing platform developed by Microsoft. It offers management, access and development of applications and services through its global infrastructure.',
        logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Microsoft_Azure.svg/2048px-Microsoft_Azure.svg.png'
      },
      label: 'Microsoft Azure',
      value: 'azure'
    }, {
      customRendererData: {
        description: 'Google Cloud Platform, offered by Google, is a suite of cloud computing services that provides a series of modular cloud services including computing, data storage, data analytics, and machine learning, alongside a set of management tools.',
        logo: 'https://upload.wikimedia.org/wikipedia/commons/0/01/Google-cloud-platform.svg'
      },
      label: 'Google Cloud Platform',
      value: 'gcp'
    }]
  }];
  return <Select items={items} multiple>
      <SelectControl customItemRenderer={({
      selectedItems    }) => <span style={{
      display: 'flex',
      flexFlow: 'row',
      gap: '8px',
      flexWrap: 'wrap'
    }}>
        {selectedItems.map((item, idx) => <span style={{
        display: 'flex',
        flexFlow: 'row',
        gap: '4px',
        alignItems: 'center'
      }} key={item.value}>
              <img alt={item.label} height={15} src={item.customRendererData?.logo} width={15} />
              <span>{item.label}{idx < selectedItems.length - 1 && ', '}</span>
            </span>)}
      </span>} />
      <SelectContent customGroupRenderer={({
      customData,
      label    }) => <div style={{
      display: 'flex',
      flexFlow: 'row',
      columnGap: '8px',
      alignItems: 'center'
    }}>
            <img alt="flag" height={20} src={customData?.flag} width={30} />
            <span>{label}</span>
          </div>} customOptionRenderer={({
      customData,
      label    }) => <div style={{
      display: 'flex',
      flexFlow: 'row',
      columnGap: '8px',
      alignItems: 'center',
      padding: '8px 0'
    }}>
            <img alt={label} height={50} src={customData?.logo} width={50} />
            <div style={{
        display: 'flex',
        flexFlow: 'column',
        rowGap: '8px'
      }}>
              <span style={{
          fontWeight: 'bold'
        }}>{label}</span>
              <span>{customData?.description}</span>
            </div>
          </div>} />
    </Select>;
}
```

### Form field

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Select a Web hosting      </FormFieldLabel>
      <Select items={[{
      label: '1 vCore 2,4 GHz, 2 Go RAM',
      value: 'hosting-1'
    }, {
      label: '1 vCore 2,4 GHz, 4 Go RAM',
      value: 'hosting-2'
    }, {
      label: '2 vCores 2,4 GHz, 8 Go RAM',
      value: 'hosting-3'
    }]}>
        <SelectControl />
        <SelectContent />
      </Select>
    </FormField>
}
```

## Recipes

---

Config Tile

VPS 1

4 vCore8 Go RAM100 Go 1 day automated backup Unlimited traffic 200 Mbps

12 months6 monthsNo commitment

From€24.46ex. VAT/monthor €13.19 incl. VAT/month

Email Field

Email- mandatory

@

.fr.com.dev

The part before the email address (the text before the @) must follow these guidelines:

-   It must end with a letter or a number
-   Allowed special characters are: ".", "_", "-"
-   Special characters cannot be placed next to each other

Feature List Product Card

WEB HOSTING

NewBest seller

Performance

For demanding online stores and projects.

1 vCore 2,4 GHz, 2 Go RAM1 vCore 2,4 GHz, 4 Go RAM2 vCores 2,4 GHz, 8 Go RAM

From

€24.46ex. VAT/month

or €13.19 incl. VAT/monthfor a 24-month registration

Minimum 2-year registration €100 free with a 5-year registration

Installation fee:Free

-   -   Unlimited websites
        
    -   High power level
        
    -   1 domain name free for the first
        
    -   500 GB SSD storage
        
    -   1,000 email addresses
        
-   1-click CMS
    
    -   WordPress
        
    -   Joomla!
        
    -   Drupal
        
    -   Prestashop
        
-   Database
    
    -   4 x 1 GB databases
        
    -   8 GB Web Cloud Databases
        
-   Security
    
    -   Unlimited free SSL
        
    -   Anti-DDoS protection
        
    -   Anti-virus and anti-spam
        
    -   Daily backups
        
-   Performance
    
    -   99.9% observed availability
        
    -   Guaranteed resources
        
    -   Unlimited traffic
        
    -   Service continuity
        
    -   Boost option to withstand temporary traffic spikes
        
-   Support and additional services
    
    -   Git
        
    -   Standard support
        
    -   SSH access
        
    -   CDN Basic

## React Components/Select

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultOpen` | `` | No |  | The initial open state of the select. Use when you don't need to control the open state of the select. |
| `defaultValue` | `` | No |  | The initial selected value(s). Use when you don't need to control the selected value(s) of the select. |
| `disabled` | `` | No | false | Whether the component is disabled. |
| `fitControlWidth` | `` | No | true | @deprecated Use overlayConfig.sameWidth instead |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `items` | `` | No | [] | The list of items |
| `multiple` | `` | No | false | Allows multiple selection and define how it should be rendered. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onOpenChange` | `` | No |  | Callback fired when the select open state changes. |
| `onValueChange` | `` | No |  | Callback fired when the value(s) changes. |
| `open` | `` | No |  | The controlled open state of the select. |
| `overlayConfig` | `` | No |  | The overlay configuration. |
| `positionerStyle` | `` | No |  | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |
| `readOnly` | `` | No | false | Whether the component is readonly. |
| `required` | `` | No |  | Whether the component is required. |
| `value` | `` | No |  | The controlled selected value(s). |


## Subcomponents


### SelectContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. |
| `customGroupRenderer` | `` | No |  | Custom render for each group item. |
| `customOptionRenderer` | `` | No |  | Custom render for each option item. |



### SelectControl



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `customItemRenderer` | `` | No |  | Custom render for the selected item(s). |
| `multipleSelectionLabel` | `` | No |  | Label displayed on multiple selection when in "merge" mode. |
| `placeholder` | `` | No |  | The placeholder text to display in the select. |


## Examples


### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Select a Web hosting
      </FormFieldLabel>

      <Select items={[{
      label: '1 vCore 2,4 GHz, 2 Go RAM',
      value: 'hosting-1'
    }, {
      label: '1 vCore 2,4 GHz, 4 Go RAM',
      value: 'hosting-2'
    }, {
      label: '2 vCores 2,4 GHz, 8 Go RAM',
      value: 'hosting-3'
    }]}>
        <SelectControl />

        <SelectContent />
      </Select>
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  parameters: {
    layout: 'start'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    height: '230px'
  }}>
      <Select items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]} open overlayConfig={{
      flip: false
    }}>
        <SelectControl placeholder="Select one or more pets" style={{
        width: '230px'
      }} />

        <SelectContent createPortal={false} />
      </Select>
    </div>
}
```

### Custom Renderer

```tsx
{
  globals: {
    imports: `import { Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    type CustomData = {
      description?: string;
      flag?: string;
      logo?: string;
    };
    const items: SelectItem<CustomData>[] = [{
      customRendererData: {
        flag: 'https://upload.wikimedia.org/wikipedia/commons/b/b7/Flag_of_Europe.svg'
      },
      label: 'EU providers',
      options: [{
        customRendererData: {
          description: 'OVH, legally OVH Groupe SA, is a French cloud computing company which offers VPS, dedicated servers and other web services. As of 2016 OVH owned the world\'s largest data center in surface area. As of 2019, it was the largest hosting provider in Europe, and the third largest in the world based on physical servers.',
          logo: 'https://ovh.github.io/manager/ovhcloud-logo.webp'
        },
        label: 'OVH Cloud',
        value: 'ovh'
      }]
    }, {
      customRendererData: {
        flag: 'https://upload.wikimedia.org/wikipedia/en/a/a4/Flag_of_the_United_States.svg'
      },
      label: 'US providers',
      options: [{
        customRendererData: {
          description: 'Amazon Web Services, Inc. is a subsidiary of Amazon that provides on-demand cloud computing platforms and APIs to individuals, companies, and governments, on a metered, pay-as-you-go basis. Clients will often use this in combination with autoscaling.',
          logo: 'https://cdn.icon-icons.com/icons2/2407/PNG/512/aws_icon_146074.png'
        },
        label: 'Amazon Web Services',
        value: 'aws'
      }, {
        customRendererData: {
          description: 'Microsoft Azure, often referred to as just Azure, is a cloud computing platform developed by Microsoft. It offers management, access and development of applications and services through its global infrastructure.',
          logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Microsoft_Azure.svg/2048px-Microsoft_Azure.svg.png'
        },
        label: 'Microsoft Azure',
        value: 'azure'
      }, {
        customRendererData: {
          description: 'Google Cloud Platform, offered by Google, is a suite of cloud computing services that provides a series of modular cloud services including computing, data storage, data analytics, and machine learning, alongside a set of management tools.',
          logo: 'https://upload.wikimedia.org/wikipedia/commons/0/01/Google-cloud-platform.svg'
        },
        label: 'Google Cloud Platform',
        value: 'gcp'
      }]
    }];
    return <Select items={items} multiple>
        <SelectControl customItemRenderer={({
        selectedItems
      }) => <span style={{
        display: 'flex',
        flexFlow: 'row',
        gap: '8px',
        flexWrap: 'wrap'
      }}>
          {selectedItems.map((item, idx) => <span style={{
          display: 'flex',
          flexFlow: 'row',
          gap: '4px',
          alignItems: 'center'
        }} key={item.value}>
                <img alt={item.label} height={15} src={item.customRendererData?.logo} width={15} />

                <span>{item.label}{idx < selectedItems.length - 1 && ', '}</span>
              </span>)}
        </span>} />

        <SelectContent customGroupRenderer={({
        customData,
        label
      }) => <div style={{
        display: 'flex',
        flexFlow: 'row',
        columnGap: '8px',
        alignItems: 'center'
      }}>
              <img alt="flag" height={20} src={customData?.flag} width={30} />

              <span>{label}</span>
            </div>} customOptionRenderer={({
        customData,
        label
      }) => <div style={{
        display: 'flex',
        flexFlow: 'row',
        columnGap: '8px',
        alignItems: 'center',
        padding: '8px 0'
      }}>
              <img alt={label} height={50} src={customData?.logo} width={50} />
              <div style={{
          display: 'flex',
          flexFlow: 'column',
          rowGap: '8px'
        }}>
                <span style={{
            fontWeight: 'bold'
          }}>{label}</span>
                <span>{customData?.description}</span>
              </div>
            </div>} />
      </Select>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Select items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <SelectControl />

      <SelectContent />
    </Select>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Select disabled={arg.disabled} invalid={arg.invalid} items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]} multiple={arg.multiple} overlayConfig={{
    sameWidth: arg.sameWidth
  }} readOnly={arg.readOnly}>
      <SelectControl multipleSelectionLabel={arg.multipleSelectionLabel} placeholder={arg.placeholder} />

      <SelectContent />
    </Select>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    multiple: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'select'
      },
      options: [true, false, 'merge']
    },
    multipleSelectionLabel: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    placeholder: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    sameWidth: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    }
  })
}
```

### Disabled

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { TEXT_PRESET, Select, SelectContent, SelectControl, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Select disabled items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]}>
        <Text htmlFor="disabled" preset={TEXT_PRESET.label}>
          Disabled
        </Text>

        <SelectControl id="disabled" placeholder="Select one or more pets" />

        <SelectContent />
      </Select>

      <Select items={[{
      label: 'Dog',
      value: 'dog',
      disabled: true
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot',
      disabled: true
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]}>
        <Text htmlFor="disabled-options" preset={TEXT_PRESET.label}>
          Disabled options
        </Text>

        <SelectControl id="disabled-options" placeholder="Select one or more pets" />

        <SelectContent />
      </Select>

      <Select items={[{
      label: 'Europe',
      options: [{
        label: 'France',
        value: 'fr'
      }, {
        label: 'Germany',
        value: 'de',
        disabled: true
      }, {
        label: 'Italy',
        value: 'it'
      }]
    }, {
      disabled: true,
      label: 'Asia',
      options: [{
        label: 'China',
        value: 'cn',
        disabled: true
      }, {
        label: 'Japan',
        value: 'jp',
        disabled: true
      }, {
        label: 'Russia',
        value: 'ru',
        disabled: true
      }]
    }, {
      label: 'North America',
      options: [{
        label: 'Canada',
        value: 'ca'
      }, {
        label: 'Mexico',
        value: 'mx'
      }, {
        label: 'United States of America',
        value: 'us'
      }]
    }]}>
        <Text htmlFor="disabled-group" preset={TEXT_PRESET.label}>
          Disabled group or group option
        </Text>

        <SelectControl id="disabled-group" />

        <SelectContent />
      </Select>
    </>
}
```

### Group

```tsx
{
  globals: {
    imports: `import { Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Select items={[{
    label: 'Europe',
    options: [{
      label: 'France',
      value: 'fr'
    }, {
      label: 'Germany',
      value: 'de'
    }, {
      label: 'Italy',
      value: 'it'
    }]
  }, {
    label: 'Asia',
    options: [{
      label: 'China',
      value: 'cn'
    }, {
      label: 'Japan',
      value: 'jp'
    }, {
      label: 'Russia',
      value: 'ru'
    }]
  }, {
    label: 'North America',
    options: [{
      label: 'Canada',
      value: 'ca'
    }, {
      label: 'Mexico',
      value: 'mx'
    }, {
      label: 'United States of America',
      value: 'us'
    }]
  }]}>
      <SelectControl />

      <SelectContent />
    </Select>
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Select a Web hosting
      </FormFieldLabel>

      <Select items={[{
      label: '1 vCore 2,4 GHz, 2 Go RAM',
      value: 'hosting-1'
    }, {
      label: '1 vCore 2,4 GHz, 4 Go RAM',
      value: 'hosting-2'
    }, {
      label: '2 vCores 2,4 GHz, 8 Go RAM',
      value: 'hosting-3'
    }]}>
        <SelectControl />

        <SelectContent />
      </Select>
    </FormField>
}
```

### Multiple

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { TEXT_PRESET, Select, SelectContent, SelectControl, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Select items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]} multiple>
        <Text htmlFor="multiple" preset={TEXT_PRESET.label}>
          Default multiple selection
        </Text>

        <SelectControl id="multiple" placeholder="Select one or more pets" />

        <SelectContent />
      </Select>

      <Select items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }, {
      label: 'Hamster',
      value: 'hamster'
    }, {
      label: 'Parrot',
      value: 'parrot'
    }, {
      label: 'Spider',
      value: 'spider'
    }, {
      label: 'Goldfish',
      value: 'goldfish'
    }]} multiple="merge">
        <Text htmlFor="multiple-merged" preset={TEXT_PRESET.label}>
          Merged multiple selection
        </Text>

        <SelectControl id="multiple-merged" placeholder="Select one or more pets" />

        <SelectContent />
      </Select>
    </>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Select items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]}>
      <SelectControl placeholder="Select one or more pets" />

      <SelectContent />
    </Select>
}
```

### Readonly

```tsx
{
  globals: {
    imports: `import { Select, SelectContent, SelectControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Select items={[{
    label: 'Dog',
    value: 'dog'
  }, {
    label: 'Cat',
    value: 'cat'
  }, {
    label: 'Hamster',
    value: 'hamster'
  }, {
    label: 'Parrot',
    value: 'parrot'
  }, {
    label: 'Spider',
    value: 'spider'
  }, {
    label: 'Goldfish',
    value: 'goldfish'
  }]} readOnly>
      <SelectControl placeholder="Select one or more pets" />

      <SelectContent />
    </Select>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    alignItems: 'flex-start'
  }}>
      <Select items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <SelectControl placeholder="Default" />
        <SelectContent createPortal={false} />
      </Select>

      <Select multiple items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <SelectControl placeholder="Multiple" />
        <SelectContent createPortal={false} />
      </Select>

      <Select disabled items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <SelectControl placeholder="Disabled" />
        <SelectContent createPortal={false} />
      </Select>

      <Select readOnly items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <SelectControl placeholder="Read only" />
        <SelectContent createPortal={false} />
      </Select>

      <Select invalid items={[{
      label: 'Dog',
      value: 'dog'
    }, {
      label: 'Cat',
      value: 'cat'
    }]}>
        <SelectControl placeholder="Invalid" />
        <SelectContent createPortal={false} />
      </Select>
    </div>
}
```

## React Components

# Skeleton

_**Skeleton** component indicates that data is loading. It improves the perceived loading time for the user._

## Overview

---

**Skeleton** indicates, through and animation, that content is loading and that the screen is not frozen.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Skeleton</td></tr><tr><th scope="row">Also known as</th><td>Skeleton loader</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=52-10250" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/skeleton" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-skeleton--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

A **Skeleton** can be used in a variety of contexts such as in Cards, lists, and table content.

### Spinner vs Skeleton

Both  and **Skeleton** indicate that content is loading, but they serve different purposes:

-   Use a Spinner when the content or result is unknown and the user must wait. For example, after clicking a button or loading data from an API
    
-   Use a **Skeleton** when the structure of the content is known, but the actual data hasn't loaded yet. It helps set expectations and makes the wait feel shorter
    

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Skeleton to indicate loading states for dynamic content, such as user data or content blocks |
| - Prefer Skeleton when content will take more than a moment to load (usually > 500ms) |
| - Ensure Skeleton is replaced with real content as soon as it is available |

| ❌ Don't |
| --- |
| - Don't use Skeleton for static content that doesn't change, load it normally without placeholders |
| - Don't apply Skeleton to interactive elements like buttons or inputs. Use Spinner or loading states instead |
| - Don't show Skeleton for very short loading delays, it may feel like a glitch or visual noise |
| - Don't animate excessively or use distracting visuals. Skeleton should feel smooth and unobtrusive |

## Placement

---

A **Skeleton** can be placed whenever needed to indicate a content is loading.

## Behavior

---

The **Skeleton** component has a linear animation to show to indicates a content is loading.

## Navigation

---

The **Skeleton** component is non-interactive and does not receive keyboard focus. It is purely visual and serves as a placeholder for content loading, without affecting keyboard navigation.

## Accessibility

---

The **Skeleton** component is purely decorative. Its role is to visually indicate that content is loading, but it does not carry any meaningful information for assistive technologies. For this reason, it is hidden from screen readers.

### Indicating loading state

To ensure screen reader users are aware that content is being loaded, you should set [aria-busy](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-busy) on the container that will eventually receive the content.

```jsx
<div aria-busy="true">
  <Skeleton />
</div>
```

Using this approach ensures that assistive technologies are aware that content is being updated, allowing them to wait before announcing changes.

## React Components

# Skeleton

## Overview

---

## Anatomy

---

Skeleton

---

## Skeleton

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div#attributes) . |

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-skeleton-background-color | var(--ods-color-neutral-050) | 
 |
| --ods-skeleton-background-color-image | linear-gradient(-90deg, var(--ods-skeleton-background-color), var(--ods-skeleton-transition-background-color) 46%, var(--ods-skeleton-transition-background-color) 61%, var(--ods-skeleton-background-color)) | 

 |
| --ods-skeleton-transition-background-color | var(--ods-color-neutral-100) | 

 |

### Default

```jsx
<Skeleton />
```

## Recipes

---

No recipe defined for now.

## React Components/Skeleton

## Examples


### Accessibility Bad Practice Loading

```tsx
{
  globals: {
    imports: 'import { Skeleton } from \'@ovhcloud/ods-react\';'
  },
  tags: ['!dev'],
  render: ({}) => <div>
      <Skeleton />
    </div>
}
```

### Accessibility Loading

```tsx
{
  globals: {
    imports: 'import { Skeleton } from \'@ovhcloud/ods-react\';'
  },
  tags: ['!dev'],
  render: ({}) => <div aria-busy="true">
      <Skeleton />
    </div>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <div style={{
    width: '200px'
  }}>
      <Skeleton />
    </div>
}
```

### Default

```tsx
{
  globals: {
    imports: 'import { Skeleton } from \'@ovhcloud/ods-react\';'
  },
  tags: ['!dev'],
  render: ({}) => <Skeleton />
}
```

### Demo

```tsx
{}
```

### Overview

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Skeleton />
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px'
  }}>
      <Skeleton />
      <Skeleton />
      <Skeleton />
    </div>
}
```

## React Components

# Spinner

_A visual indicator that a process is happening in the background but the interface is not yet ready for interaction._

## Overview

---

A visual indicator that a process is happening in the background but the interface is not yet ready for interaction.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Spinner</td></tr><tr><th scope="row">Also known as</th><td>Loading, Spin, Circular Progress</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=52-10340" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/spinner" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-spinner--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Spinner** component is used as a fallback when complex content is loading in the background.

### Spinner vs Skeleton

Both **Spinner** and  indicate that content is loading, but they serve different purposes:

-   Use a **Spinner** when the content or result is unknown and the user must wait. For example, after clicking a button or loading data from an API
    
-   Use a Skeleton when the structure of the content is known, but the actual data hasn't loaded yet. It helps set expectations and makes the wait feel shorter
    

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Spinner to indicate indeterminate loading when the duration is unknown or cannot be measured |
| - Use a Spinner when complex or background processes are happening (e.g. fetching filtered data, saving a form) |
| - Display a Spinner if the loading is expected to take more than a couple of seconds, to reassure the user |
| - Combine the Spinner with contextual messaging (e.g. "Loading [...]" or "Saving your changes") to clarify what's happening |

| ❌ Don't |
| --- |
| - Don't use multiple Spinners on the same page, it creates confusion and visual noise. Use a single centralized Spinner or loading overlay |
| - Don't use a Spinner for text or layout placeholders, use the Skeleton component instead, which better preserves structure |
| - Don't use a Spinner if the loading duration is extremely short, this can feel like a flicker or visual glitch |
| - Don't place Spinners in interactive elements like buttons without also disabling them and explaining the ongoing action |

## Placement

---

**Spinner** can be placed where needed, whether it's centered on an overlay or specific container/content in the page.

**Spinner** can widen to match its container.

## Behavior

---

The **Spinner** component has a spinning animation to show to the user that the background process is moving on.

## Variation

---

### Color

-   **Primary** (default): used in contexts where the **Spinner** is tied to the main brand action
-   **Neutral**: used for secondary or subtle loading indicators, and commonly displayed in disabled parent components to indicate background activity without drawing too much attention
-   **White**: designed for use on dark or colored backgrounds to ensure proper contrast and visibility

## Navigation

---

The **Spinner** component is non-interactive and does not receive keyboard focus. It is purely visual and serves as a loading indicator, without affecting keyboard navigation.

## Accessibility

---

The **Spinner** component does not automatically provide enough context for assistive technologies. To ensure accessibility, additional attributes must be applied to the container that holds the loading content.

### Indicating loading state

The **Spinner** itself does not convey the loading state. This responsibility falls on the container where content is being loaded by using [aria-busy](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-busy) and [aria-live](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-live) .

```jsx
<div
  aria-busy="true"
  aria-live="polite"
>
  <Spinner />
</div>
```

Explanations:

-   `aria-busy="true"` informs assistive technologies that content is being updated.
-   `aria-live="polite"` ensures that the loading message is announced when necessary without interrupting.

This approach ensures that users with assistive technologies understand when loading starts and ends.

Once the content is loaded, aria-busy should be set to false.

### Linking the Spinner to a loading description

If the **Spinner** is associated with a specific loading action, it should be explicitly labeled using [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby) or [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) .

```jsx
<Spinner aria-label="Loading user profile" />
```

Screen readers will announce the label, the spinner and its current state.

Loading user profile

```jsx
<div>
  <span id="loading-text">
    Loading user profile  </span>
  <Spinner aria-labelledby="loading-text" />
</div>
```

Screen readers will announce the label, the spinner and its current state.

## React Components

# Spinner

## Overview

---

## Anatomy

---

Spinner

---

## Spinner

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |
| 
color

 | `SPINNER_COLOR` | - | `SPINNER_COLOR.primary` | The color preset to use. |
| 

size

 | `SPINNER_SIZE` | - | `SPINNER_SIZE.md` | The size preset to use. |

## Enums

---

### SPINNER_COLOR

-   neutral =`"neutral"`
-   primary =`"primary"`

### SPINNER_SIZE

-   lg =`"lg"`
-   md =`"md"`
-   sm =`"sm"`
-   xs =`"xs"`

## Examples

---

### Default

```jsx
<Spinner />
```

### Color

```jsx
<>
  <Spinner color="neutral" />
  <Spinner color="primary" />
</>
```

### Size

```jsx
{
  globals: {
    imports: `import { SPINNER_SIZE, Spinner } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Spinner size={SPINNER_SIZE.xs} />
      <Spinner size={SPINNER_SIZE.sm} />
      <Spinner size={SPINNER_SIZE.md} />
      <Spinner size={SPINNER_SIZE.lg} />
    </>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Spinner

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No | SPINNER_COLOR.primary | The color preset to use. |
| `size` | `` | No | SPINNER_SIZE.md | The size preset to use. |


## Examples


### Accessibility Aria Busy Aria Live

```tsx
{
  globals: {
    imports: `import { Spinner } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <div aria-busy="true" aria-live="polite">
      <Spinner />
    </div>
}
```

### Accessibility Aria Label

```tsx
{
  globals: {
    imports: `import { Spinner } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Spinner aria-label="Loading user profile" />
}
```

### Accessibility Aria Labelled By

```tsx
{
  globals: {
    imports: `import { Spinner } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <div>
      <span id="loading-text">
        Loading user profile
      </span>

      <Spinner aria-labelledby="loading-text" />
    </div>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Spinner />
}
```

### Color

```tsx
{
  globals: {
    imports: `import { SPINNER_COLOR, Spinner } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Spinner color={SPINNER_COLOR.neutral} />
      <Spinner color={SPINNER_COLOR.primary} />
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Spinner } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Spinner />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    color: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'SPINNER_COLOR'
        }
      },
      control: {
        type: 'select'
      },
      options: SPINNER_COLORS
    },
    size: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'SPINNER_SIZE'
        }
      },
      control: {
        type: 'select'
      },
      options: SPINNER_SIZES
    }
  })
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Spinner />
}
```

### Size

```tsx
{
  globals: {
    imports: `import { SPINNER_SIZE, Spinner } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Spinner size={SPINNER_SIZE.xs} />
      <Spinner size={SPINNER_SIZE.sm} />
      <Spinner size={SPINNER_SIZE.md} />
      <Spinner size={SPINNER_SIZE.lg} />
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'inline-flex',
    flexFlow: 'row',
    gap: '12px',
    alignItems: 'center'
  }}>
      <Spinner />
      <Spinner size={SPINNER_SIZE.sm} />
      <Spinner size={SPINNER_SIZE.lg} />
      <Spinner color={SPINNER_COLOR.neutral} />
      <Spinner color={SPINNER_COLOR.primary} />
    </div>
}
```

## React Components

# Switch

_A **Switch** allows users to quickly and easily switch between several states, actions or options in a row._

Component is now deprecated and will be removed in a future major release. You can use different components instead depending on your use-case:

-   managing navigation: move to  using the switch variant.
-   managing option activation: move to a .
-   as a form element: move to a .

## Overview

---

A **Switch** allows users to quickly and easily switch between several states, actions or options in a row.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Switch</td></tr><tr><th scope="row">Also known as</th><td>Button group, Segmented control, Toggle group</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=52-10578" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/switch" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-switch--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

A **Switch** is used to switch between multiple states, actions or options (up-to 4).

It can be used for many use cases such as selecting a state within a group of states or filtering.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Switch when the user must choose one active option among 2 to 4 closely related states or actions |
| - Ensure each Switch item has a clear, descriptive label |
| - Use a Switch when immediate feedback or real-time filtering is expected upon selection |

| ❌ Don't |
| --- |
| - Don't use a Switch for more than 4 options. Prefer Radio Group, Checkbox, or Select instead depending on your use case |
| - Don't use a Switch for unrelated options, they should belong to a common category or context |
| - Don't use a Switch if selecting an option does not produce an immediate effect. Use a Checkbox or another control instead |
| - Don't rely solely on icons, add text for clarity |

### Best Practices in Context

1.  **Switch**
2.  **Active toggle button**
3.  **Toggle button**
4.  **Label**

## Placement

---

A **Switch** can be used in a page when the user needs to select a choice from multiple states or items. It may replace two radio buttons or a single checkbox to allow users to choose between several states.

By default, the width of a **Switch** item is defined by the length of content. A custom width can be set so all items on the track will have the same size regardless of content length.

## Behavior

---

The user can switch between states by clicking the **Switch** items, and it has an immediate effect.

## Navigation

---

### Focus Management

When the **Switch** receives focus, it is set on the currently selected item, or on the first item if none is selected.

Each individual **Switch** item is focusable unless disabled. A disabled item cannot receive focus or be activated.

Focus remains within the group when navigating between items using arrow keys.

### General Keyboard Shortcuts

Pressing Tab moves focus to the selected item or the first item in the group.

Pressing Shift + Tab moves focus to the previous focusable element outside the **Switch** group.

Pressing Arrow Right or Arrow Down moves focus to the next item in the group.

Pressing Arrow Left or Arrow Up moves focus to the previous item in the group.

Pressing Home (or fn + Arrow Left) moves focus to the first item.

Pressing End (or fn + Arrow Right) moves focus to the last item.

Pressing Space or Enter activates or deactivates the focused item, updating the selection immediately.

## Accessibility

---

To ensure proper accessibility, **Switch** must be correctly labeled.

### Always provide an explicit label

Every **Switch** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either an [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) or an [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby) attribute.

```jsx
<Switch aria-label="Select an item">
  <SwitchItem value="item-1">
    Item 1  </SwitchItem>
  <SwitchItem value="item-2">
    Item 2  </SwitchItem>
  <SwitchItem value="item-3">
    Item 3  </SwitchItem>
</Switch>
```

Screen readers will announce the label and the option information (text, position, selection state).

Select an item:

```jsx
<>
  <Text
    id="switch-label"
    preset="label"
  >
    Select an item:  </Text>
  <Switch aria-labelledby="switch-label">
    <SwitchItem value="item-1">
      Item 1    </SwitchItem>
    <SwitchItem value="item-2">
      Item 2    </SwitchItem>
    <SwitchItem value="item-3">
      Item 3    </SwitchItem>
  </Switch>
</>
```

Screen readers will announce the label and the option information (text, position, selection state).

## React Components

# Switch

## Overview

---

Component is now deprecated and will be removed in a future major release. You can use different components instead depending on your use-case:

-   managing navigation: move to  using the switch variant.
-   managing option activation: move to a .
-   as a form element: move to a .

## Anatomy

---

Switch

SwitchItem

---

## Switch

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultValue

 | `string` | - | `undefined` | The initial value of the selected item. Use when you don't need to control the value of the switch. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

onValueChange

 | `(detail: SwitchValueChangeDetail) => void` | - | `undefined` | Callback fired when the value changes. |
| 

size

 | `SWITCH_SIZE` | - | `SWITCH_SIZE.md` | The size preset to use. |
| 

value

 | `string` | - | `undefined` | The controlled value of the selected item. |

## SwitchItem

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
value

 | `string` |  | `undefined` | The value of the switch item. |

## Enums

---

### SWITCH_SIZE

-   md =`"md"`
-   sm =`"sm"`

## Interfaces

---

### SwitchValueChangeDetail

-   `value: string`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-switch-background-color | var(--ods-color-neutral-100) | 
 |
| --ods-switch-border-radius | calc(var(--ods-theme-border-radius) * 2) | 

 |
| --ods-switch-column-gap | calc(var(--ods-theme-column-gap) / 2) | 

 |
| --ods-switch-item-background-color | inherit | 

 |
| --ods-switch-item-background-color-checked | var(--ods-color-primary-600) | 

 |
| --ods-switch-item-background-color-checked-disabled | var(--ods-color-neutral-500) | 

 |
| --ods-switch-item-background-color-hover | var(--ods-color-primary-100) | 

 |
| --ods-switch-item-text-color | var(--ods-color-primary-500) | 

 |
| --ods-switch-item-text-color-checked | var(--ods-theme-input-text-color-checked) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Switch, SwitchItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Switch>
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>
}
```

### Checked

```jsx
{
  globals: {
    imports: `import { Switch, SwitchItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Switch defaultValue="item-1">
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Switch, SwitchItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Switch disabled>
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>
}
```

### Sizes

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { SWITCH_SIZE, Switch, SwitchItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Switch size={SWITCH_SIZE.sm}>
        <SwitchItem value="item-1">Item 1</SwitchItem>
        <SwitchItem value="item-2">Item 2</SwitchItem>
        <SwitchItem value="item-3">Item 3</SwitchItem>
      </Switch>
      <Switch size={SWITCH_SIZE.md}>
        <SwitchItem value="item-1">Item 1</SwitchItem>
        <SwitchItem value="item-2">Item 2</SwitchItem>
        <SwitchItem value="item-3">Item 3</SwitchItem>
      </Switch>
    </>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Switch

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultValue` | `` | No |  | The initial value of the selected item. Use when you don't need to control the value of the switch. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `onValueChange` | `` | No |  | Callback fired when the value changes. |
| `size` | `` | No | SWITCH_SIZE.md | The size preset to use. |
| `value` | `` | No |  | The controlled value of the selected item. |


## Subcomponents


### SwitchItem



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `value` | `` | Yes |  | The value of the switch item. |


## Examples


### Accessibility Aria Label

```tsx
{
  globals: {
    imports: `import { Switch, SwitchItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Switch aria-label="Select an item">
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>
}
```

### Accessibility Aria Labelledby

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    alignItems: 'start'
  }}>{story()}</div>],
  globals: {
    imports: `import { TEXT_PRESET, Switch, SwitchItem, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Text id="switch-label" preset={TEXT_PRESET.label}>
        Select an item:
      </Text>

      <Switch aria-labelledby="switch-label">
        <SwitchItem value="item-1">Item 1</SwitchItem>
        <SwitchItem value="item-2">Item 2</SwitchItem>
        <SwitchItem value="item-3">Item 3</SwitchItem>
      </Switch>
    </>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Switch defaultValue="item-1">
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>
}
```

### Checked

```tsx
{
  globals: {
    imports: `import { Switch, SwitchItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Switch defaultValue="item-1">
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Switch, SwitchItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Switch>
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>
}
```

### Demo

```tsx
{
  render: arg => <Switch {...arg}>
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    size: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'SWITCH_SIZE'
        }
      },
      control: {
        type: 'select'
      },
      options: SWITCH_SIZES
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Switch, SwitchItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Switch disabled>
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Switch defaultValue="item-1">
      <SwitchItem value="item-1">Item 1</SwitchItem>
      <SwitchItem value="item-2">Item 2</SwitchItem>
      <SwitchItem value="item-3">Item 3</SwitchItem>
    </Switch>
}
```

### Sizes

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { SWITCH_SIZE, Switch, SwitchItem } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Switch size={SWITCH_SIZE.sm}>
        <SwitchItem value="item-1">Item 1</SwitchItem>
        <SwitchItem value="item-2">Item 2</SwitchItem>
        <SwitchItem value="item-3">Item 3</SwitchItem>
      </Switch>

      <Switch size={SWITCH_SIZE.md}>
        <SwitchItem value="item-1">Item 1</SwitchItem>
        <SwitchItem value="item-2">Item 2</SwitchItem>
        <SwitchItem value="item-3">Item 3</SwitchItem>
      </Switch>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'column',
    rowGap: '16px'
  }}>
      <Switch style={{
      alignSelf: 'start'
    }}>
        <SwitchItem value="item-1">Item 1</SwitchItem>
        <SwitchItem value="item-2">Item 2</SwitchItem>
        <SwitchItem value="item-3">Item 3</SwitchItem>
      </Switch>

      <Switch disabled style={{
      alignSelf: 'start'
    }}>
        <SwitchItem value="item-1">Item 1</SwitchItem>
        <SwitchItem value="item-2">Item 2</SwitchItem>
        <SwitchItem value="item-3">Item 3</SwitchItem>
      </Switch>

      <Switch size={SWITCH_SIZE.sm} style={{
      alignSelf: 'start'
    }}>
        <SwitchItem value="item-1">Small 1</SwitchItem>
        <SwitchItem value="item-2">Small 2</SwitchItem>
      </Switch>
    </div>
}
```

## React Components

# Table

_A component to display data in a tabular format._

Front-end web developer course 2021
| Person | Most interest in | Age |
| --- | --- | --- |
| Chris | HTML tables | 22 |
| Dennis | Web accessibility | 45 |
| Sarah | JavaScript frameworks | 29 |
| Karen | Web performance | 36 |

## Overview

---

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Table</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=93-12118" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/table" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-table--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

### Data Table vs Table

`Table`:

-   Static data display.
-   Limited or no interaction.
-   Often used for simple layouts or read-only content.

`Data Table`:

-   Interactive and stateful component.
-   Supports sorting, selection, and actions.
-   Integrated with application logic through composition.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Table to present structured data in a clear and readable layout |
| - Limit the number of columns to 9 or fewer to maintain readability and avoid horizontal scrolling |
| - Use concise, meaningful headers, and add a Tooltip or description if more context is needed |
| - Display Skeletons in cells when data is loading asynchronously, rather than leaving them blank |
| - Ensure the Table is responsive by allowing columns to adapt to content and screen size |

| ❌ Don't |
| --- |
| - Don't use the Table for layout or alignment purposes, they are meant for displaying tabular data only |
| - Don't use the Table component to display large datasets (e.g. 30+ rows) without pagination or a `Load more` button |
| - Don't overload rows with too much content per cell, keep data atomic and easy to scan |
| - Don't leave column headers unclear or overly abbreviated |
| - Don't make the Table scroll in multiple directions unless absolutely necessary, to avoid poor mobile experiences |

### Best Practices in Context

1.  **Table**
2.  **Header cell**
3.  **Body cell**

## Behavior

---

**Table** component acts like native HTML table. It dynamically sizes column widths and row heights based on content and available space.

When an element is too large in a cell, the row height will adjust based on this element. It is possible to add a new line in a cell.

## Variation

---

### Size

-   **Small**: displaying compact data sets in limited spaces, making efficient use of the available area without overwhelming the user.
-   **Medium** _(default)_: default size of presenting data in a **Table**.
-   **Large**: presenting extensive data sets with more detailed information, often featuring more columns and rows to provide comprehensive visibility of the data.

### Variant

-   **Default**: provides default UI for the component, without distinctive background colors on rows/colors.
-   **Striped**: improving readability by using alternating row colors to distinguish between consecutive rows of data, making it easier for users to follow and interpret the information. We recommend to use this variant when there are more than 10 rows in the **Table**.

## Navigation

---

The **Table** component behaves like the native `<table>` element. Keyboard navigation depends on the structure and interactive elements within the Table (e.g., links, or buttons). Integrators should ensure proper focus management based on their specific implementation.

## Accessibility

---

To ensure the **Table** component is fully accessible, it is essential to follow best practices for structuring tables with the correct semantic elements (`<thead>`, `<tbody>`, `<th>`, `<td>`...).

Since **Table** implementation is handled by the integrator, we recommend referring to the [WCAG guidelines](https://www.w3.org/WAI/tutorials/tables) for accessible tables.

## React Components

# Table

## Overview

---

## Anatomy

---

Table

---

## Table

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [table attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/table#attributes) . |
| 
size

 | `TABLE_SIZE` | - | `TABLE_SIZE.md` | The size preset to use. |
| 

variant

 | `TABLE_VARIANT` | - | `TABLE_VARIANT.default` | The variant preset to use. |

## Enums

---

### TABLE_SIZE

-   lg =`"lg"`
-   md =`"md"`
-   sm =`"sm"`

### TABLE_VARIANT

-   default =`"default"`
-   striped =`"striped"`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-table-border-color | var(--ods-color-neutral-100) | 
 |
| --ods-table-cell-padding-horizontal-lg | var(--ods-theme-padding-horizontal) | 

 |
| --ods-table-cell-padding-horizontal-md | var(--ods-theme-padding-horizontal) | 

 |
| --ods-table-cell-padding-horizontal-sm | var(--ods-theme-padding-horizontal) | 

 |
| --ods-table-cell-padding-vertical-lg | calc(var(--ods-theme-padding-vertical) * 3) | 

 |
| --ods-table-cell-padding-vertical-md | calc(var(--ods-theme-padding-vertical) * 2) | 

 |
| --ods-table-cell-padding-vertical-sm | var(--ods-theme-padding-vertical) | 

 |
| --ods-table-head-background-color | var(--ods-color-neutral-050) | 

 |
| --ods-table-striped-background-color | var(--ods-color-neutral-050) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Table } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Table>
      <caption>
        Front-end web developer course 2021      </caption>
      <thead>
      <tr>
        <th scope="col">Person</th>
        <th scope="col">Most interest in</th>
        <th scope="col">Age</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <th scope="row">Chris</th>
        <td>HTML tables</td>
        <td>22</td>
      </tr>
      <tr>
        <th scope="row">Dennis</th>
        <td>Web accessibility</td>
        <td>45</td>
      </tr>
      <tr>
        <th scope="row">Sarah</th>
        <td>JavaScript frameworks</td>
        <td>29</td>
      </tr>
      <tr>
        <th scope="row">Karen</th>
        <td>Web performance</td>
        <td>36</td>
      </tr>
      </tbody>
    </Table>
}
```

### Size

Front-end web developer course 2021
| Person | Most interest in | Age |
| --- | --- | --- |
| Chris | HTML tables | 22 |
| Dennis | Web accessibility | 45 |
| Sarah | JavaScript frameworks | 29 |
| Karen | Web performance | 36 |

Front-end web developer course 2021
| Person | Most interest in | Age |
| --- | --- | --- |
| Chris | HTML tables | 22 |
| Dennis | Web accessibility | 45 |
| Sarah | JavaScript frameworks | 29 |
| Karen | Web performance | 36 |

Front-end web developer course 2021
| Person | Most interest in | Age |
| --- | --- | --- |
| Chris | HTML tables | 22 |
| Dennis | Web accessibility | 45 |
| Sarah | JavaScript frameworks | 29 |
| Karen | Web performance | 36 |

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexDirection: 'column',
    rowGap: '16px'
  }}>
      {story()}
    </div>],
  globals: {
    imports: `import { TABLE_SIZE, Table } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Table size={TABLE_SIZE.sm}>
        <caption>
          Front-end web developer course 2021        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>
      <Table size={TABLE_SIZE.md}>
        <caption>
          Front-end web developer course 2021        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>
      <Table size={TABLE_SIZE.lg}>
        <caption>
          Front-end web developer course 2021        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>
    </>
}
```

### Variant

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexDirection: 'column',
    rowGap: '16px'
  }}>
      {story()}
    </div>],
  globals: {
    imports: `import { TABLE_VARIANT, Table } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Table variant={TABLE_VARIANT.default}>
        <caption>
          Front-end web developer course 2021        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>
      <Table variant={TABLE_VARIANT.striped}>
        <caption>
          Front-end web developer course 2021        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>
    </>
}
```

### Text component caption preset

```jsx
{
  globals: {
    imports: `import { TEXT_PRESET, Table, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Table>
      <caption>
        <Text preset={TEXT_PRESET.caption}>
          Front-end web developer course 2021        </Text>
      </caption>
      <thead>
      <tr>
        <th scope="col">Person</th>
        <th scope="col">Most interest in</th>
        <th scope="col">Age</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <th scope="row">Chris</th>
        <td>HTML tables</td>
        <td>22</td>
      </tr>
      <tr>
        <th scope="row">Dennis</th>
        <td>Web accessibility</td>
        <td>45</td>
      </tr>
      <tr>
        <th scope="row">Sarah</th>
        <td>JavaScript frameworks</td>
        <td>29</td>
      </tr>
      <tr>
        <th scope="row">Karen</th>
        <td>Web performance</td>
        <td>36</td>
      </tr>
      </tbody>
    </Table>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Table

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `size` | `` | No | TABLE_SIZE.md | The size preset to use. |
| `variant` | `` | No | TABLE_VARIANT.default | The variant preset to use. |


## Examples


### Anatomy Tech

```tsx
{
  parameters: {
    layout: 'centered'
  },
  tags: ['!dev'],
  render: ({}) => <Table>
      <caption>
        Front-end web developer course 2021
      </caption>
      <thead>
      <tr>
        <th scope="col">Person</th>
        <th scope="col">Most interest in</th>
        <th scope="col">Age</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <th scope="row">Chris</th>
        <td>HTML tables</td>
        <td>22</td>
      </tr>
      <tr>
        <th scope="row">Dennis</th>
        <td>Web accessibility</td>
        <td>45</td>
      </tr>
      <tr>
        <th scope="row">Sarah</th>
        <td>JavaScript frameworks</td>
        <td>29</td>
      </tr>
      <tr>
        <th scope="row">Karen</th>
        <td>Web performance</td>
        <td>36</td>
      </tr>
      </tbody>
    </Table>
}
```

### Custom Caption

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, Table, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Table>
      <caption>
        <Text preset={TEXT_PRESET.caption}>
          Front-end web developer course 2021
        </Text>
      </caption>
      <thead>
      <tr>
        <th scope="col">Person</th>
        <th scope="col">Most interest in</th>
        <th scope="col">Age</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <th scope="row">Chris</th>
        <td>HTML tables</td>
        <td>22</td>
      </tr>
      <tr>
        <th scope="row">Dennis</th>
        <td>Web accessibility</td>
        <td>45</td>
      </tr>
      <tr>
        <th scope="row">Sarah</th>
        <td>JavaScript frameworks</td>
        <td>29</td>
      </tr>
      <tr>
        <th scope="row">Karen</th>
        <td>Web performance</td>
        <td>36</td>
      </tr>
      </tbody>
    </Table>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Table } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Table>
      <caption>
        Front-end web developer course 2021
      </caption>
      <thead>
      <tr>
        <th scope="col">Person</th>
        <th scope="col">Most interest in</th>
        <th scope="col">Age</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <th scope="row">Chris</th>
        <td>HTML tables</td>
        <td>22</td>
      </tr>
      <tr>
        <th scope="row">Dennis</th>
        <td>Web accessibility</td>
        <td>45</td>
      </tr>
      <tr>
        <th scope="row">Sarah</th>
        <td>JavaScript frameworks</td>
        <td>29</td>
      </tr>
      <tr>
        <th scope="row">Karen</th>
        <td>Web performance</td>
        <td>36</td>
      </tr>
      </tbody>
    </Table>
}
```

### Demo

```tsx
{
  render: prop => <Table {...prop}>
      <caption>
        Front-end web developer course 2021
      </caption>
      <thead>
      <tr>
        <th scope="col">Person</th>
        <th scope="col">Most interest in</th>
        <th scope="col">Age</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <th scope="row">Chris</th>
        <td>HTML tables</td>
        <td>22</td>
      </tr>
      <tr>
        <th scope="row">Dennis</th>
        <td>Web accessibility</td>
        <td>45</td>
      </tr>
      <tr>
        <th scope="row">Sarah</th>
        <td>JavaScript frameworks</td>
        <td>29</td>
      </tr>
      <tr>
        <th scope="row">Karen</th>
        <td>Web performance</td>
        <td>36</td>
      </tr>
      </tbody>
    </Table>,
  argTypes: orderControls({
    size: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'TABLE_SIZE'
        }
      },
      control: {
        type: 'select'
      },
      options: TABLE_SIZES
    },
    variant: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'TABLE_VARIANT'
        }
      },
      control: {
        type: 'select'
      },
      options: TABLE_VARIANTS
    }
  })
}
```

### Overview

```tsx
{
  parameters: {
    layout: 'centered'
  },
  tags: ['!dev'],
  render: ({}) => <Table>
      <caption>
        Front-end web developer course 2021
      </caption>
      <thead>
      <tr>
        <th scope="col">Person</th>
        <th scope="col">Most interest in</th>
        <th scope="col">Age</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <th scope="row">Chris</th>
        <td>HTML tables</td>
        <td>22</td>
      </tr>
      <tr>
        <th scope="row">Dennis</th>
        <td>Web accessibility</td>
        <td>45</td>
      </tr>
      <tr>
        <th scope="row">Sarah</th>
        <td>JavaScript frameworks</td>
        <td>29</td>
      </tr>
      <tr>
        <th scope="row">Karen</th>
        <td>Web performance</td>
        <td>36</td>
      </tr>
      </tbody>
    </Table>
}
```

### Size

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexDirection: 'column',
    rowGap: '16px'
  }}>
      {story()}
    </div>],
  globals: {
    imports: `import { TABLE_SIZE, Table } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Table size={TABLE_SIZE.sm}>
        <caption>
          Front-end web developer course 2021
        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>

      <Table size={TABLE_SIZE.md}>
        <caption>
          Front-end web developer course 2021
        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>

      <Table size={TABLE_SIZE.lg}>
        <caption>
          Front-end web developer course 2021
        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    rowGap: '16px'
  }}>
      <Table>
        <caption>Default</caption>
        <thead>
          <tr><th scope="col">Person</th><th scope="col">Most interest in</th><th scope="col">Age</th></tr>
        </thead>
        <tbody>
          <tr><th scope="row">Chris</th><td>HTML tables</td><td>22</td></tr>
          <tr><th scope="row">Dennis</th><td>Web accessibility</td><td>45</td></tr>
        </tbody>
      </Table>

      <Table variant={TABLE_VARIANT.striped}>
        <caption>Striped</caption>
        <thead>
          <tr><th scope="col">Person</th><th scope="col">Most interest in</th><th scope="col">Age</th></tr>
        </thead>
        <tbody>
          <tr><th scope="row">Sarah</th><td>JavaScript frameworks</td><td>29</td></tr>
          <tr><th scope="row">Karen</th><td>Web performance</td><td>36</td></tr>
        </tbody>
      </Table>
    </div>
}
```

### Variant

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexDirection: 'column',
    rowGap: '16px'
  }}>
      {story()}
    </div>],
  globals: {
    imports: `import { TABLE_VARIANT, Table } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Table variant={TABLE_VARIANT.default}>
        <caption>
          Front-end web developer course 2021
        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>

      <Table variant={TABLE_VARIANT.striped}>
        <caption>
          Front-end web developer course 2021
        </caption>
        <thead>
        <tr>
          <th scope="col">Person</th>
          <th scope="col">Most interest in</th>
          <th scope="col">Age</th>
        </tr>
        </thead>
        <tbody>
        <tr>
          <th scope="row">Chris</th>
          <td>HTML tables</td>
          <td>22</td>
        </tr>
        <tr>
          <th scope="row">Dennis</th>
          <td>Web accessibility</td>
          <td>45</td>
        </tr>
        <tr>
          <th scope="row">Sarah</th>
          <td>JavaScript frameworks</td>
          <td>29</td>
        </tr>
        <tr>
          <th scope="row">Karen</th>
          <td>Web performance</td>
          <td>36</td>
        </tr>
        </tbody>
      </Table>
    </>
}
```

## React Components

# Tabs

_**Tabs** are a way of navigating between multiple panels, reducing clutter and fitting more into a smaller space_

## Overview

---

**Tabs** are used to organize content by grouping similar information on the same page.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Tabs</td></tr><tr><th scope="row">Also known as</th><td>Tab Navigation, Tabbed interface</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=52-11168" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/tabs" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-tabs--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Tabs** component is ideal for dashboards, settings pages, profile sections, and any interface where multiple views need to be accessible from the same page.

They can also be used to filter content via some common denominator.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Tabs to group related content under the same page when it fits in a shared context |
| - Keep Tab labels short, clear, and scannable, use nouns or very short phrases (1–3 words max) |
| - Use Tabs for horizontal navigation, not hierarchical structure or progressive steps |
| - Do use default Tabs for main navigation |
| - Do use switch variant when Tabs act as a mode selector or view switcher (e.g., toggling between "List" and "Grid" views) |

| ❌ Don't |
| --- |
| - Don't use Tabs as a progress indicator or wizard steps |
| - Don't nest Tabs inside other Tabs, this creates a confusing and hard-to-navigate experience |
| - Don't overload the interface with too many tabs. Use dropdowns if you need more than 5 tabs |
| - Don't use long or verbose labels |
| - Don't use external links or actions as tab triggers, Tabs should only control the visibility of in-page content |
| - Avoid using the switch variant for main navigation or complex page segmentation. Prefer the default Tabs for those cases instead |

### Best Practices in Context

1.  **Tabs**
2.  **Scroll Buttons (previous/next) - optional**
3.  **Active tab**
4.  **Unselected tab**

## Placement

---

**Tabs** are often used in the top part of a web page, as it can act as a navigation focus.

## Behavior

---

By default, one of the **Tab** is always selected.

Each **Tab** can be hovered, focused and selected. A **Tab** can also act as a disabled **Tab**.

**Tabs** always stay on the same line, and can be horizontally scrolled through if they happen to not fit their container.

Each **Tab** has a panel displaying content. Selecting a **Tab** displays the corresponding panel.

The panel does not depend on the **Tab** component, it is in the developer's hands.

### Scroll Buttons

When the number of **Tabs** exceeds the visible container width, left and right scroll buttons can be displayed to allow horizontal navigation.

Clicking a button scrolls to the next or previous group of **Tabs** that were not visible.

When at the first tab, the left button is displayed but disabled.

When at the last tab, the right button is displayed but disabled.

Scroll buttons do not affect tab selection. They only change the visible portion of the **Tabs** list.

## Variation

---

### Default (standard Tabs)

Used for displaying and organizing related content within the same page or view. The default **Tabs** variant follows the standard horizontal layout, with a clear underline or border to indicate the active state. It is ideal for primary navigation within a section or module.

### Switch (sub-navigation)

The switch variant is visually distinct of **Tabs**, designed for switching between sub-views or modes within the same context. It behaves as a lightweight navigation switch between views.

## Navigation

---

### Focus Management

When focus moves to the **Tabs** component, it is set on the active **Tab**.

Once a **Tab** is focused, its associated content is also activated.

### General Keyboard Shortcuts

Pressing Tab moves focus into or out of the **Tabs** component while keeping the active **Tab** selected.

Pressing Arrow Right moves focus to the next **Tab** and activates its content.

Pressing Arrow Left moves focus to the previous **Tab** and activates its content.

Pressing Home / fn + Arrow Left moves focus to the first **Tab** and activates its content.

Pressing End / fn + Arrow Right moves focus to the last **Tab** and activates its content.

Pressing Home / End jumps to the first or last **Tab**, even if these **Tabs** are not currently visible. In this case, the **Tabs** list automatically scrolls to bring the selected **Tab** into view.

Arrow navigation moves focus to the next or previous **Tab**, even if it is not currently visible. In this case, the **Tabs** list automatically scrolls to bring the focused **Tab** into view.

## Accessibility

---

This component complies with the [Tabs WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) .

## React Components

# Tabs

## Overview

---

## Anatomy

---

Tabs

TabList

Tab

TabContent

---

## Tabs

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultValue

 | `string` | - | `undefined` | The initial value of the selected tab. Use when you don't need to control the value of the tabs. |
| 

onValueChange

 | `(event: TabsValueChangeEvent) => void` | - | `undefined` | Callback fired when the state of selected tab changes. |
| 

size

 | `TABS_SIZE` | - | `TABS_SIZE.md` | The size preset to use. |
| 

value

 | `string` | - | `undefined` | The controlled value of the selected tab. |
| 

variant

 | `TABS_VARIANT` | - | `TABS_VARIANT.default` | The variant preset to use. |
| 

withArrows

 | `boolean` | - | `undefined` | Whether the component displays navigation arrows around the tabs. |

## TabList

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## Tab

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
value

 | `string` |  | `undefined` | The value of the tab item. |

## TabContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
value

 | `string` |  | `undefined` | The value of the tab content item. |

## Enums

---

### TABS_SIZE

-   md =`"md"`
-   sm =`"sm"`
-   xs =`"xs"`

### TABS_VARIANT

-   default =`"default"`
-   switch =`"switch"`

## Interfaces

---

### TabsValueChangeEvent

-   `value: string`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-tab-background-color-hover | var(--ods-color-neutral-050) | 
 |
| --ods-tab-background-color-selected-disabled-switch | var(--ods-color-neutral-500) | 

 |
| --ods-tab-background-color-selected-switch | var(--ods-color-neutral-000) | 

 |
| --ods-tab-border-color | var(--ods-tab-list-border-color) | 

 |
| --ods-tab-border-color-disabled | var(--ods-color-neutral-100) | 

 |
| --ods-tab-border-color-hover | var(--ods-color-neutral-050) | 

 |
| --ods-tab-border-color-selected | var(--ods-color-primary-500) | 

 |
| --ods-tab-border-color-selected-disabled | var(--ods-color-neutral-500) | 

 |
| --ods-tab-border-color-selected-disabled-switch | var(--ods-color-neutral-000) | 

 |
| --ods-tab-border-radius-md | var(--ods-theme-border-radius) | 

 |
| --ods-tab-border-radius-sm | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-tab-border-radius-xs | calc(var(--ods-theme-border-radius) / 4) | 

 |
| --ods-tab-list-background-color-switch | var(--ods-color-neutral-050) | 

 |
| --ods-tab-list-border-color | var(--ods-color-neutral-100) | 

 |
| --ods-tab-list-border-radius-switch-md | calc(var(--ods-theme-border-radius) * 1.25) | 

 |
| --ods-tab-list-border-radius-switch-sm | calc(var(--ods-theme-border-radius) * 0.75) | 

 |
| --ods-tab-list-border-radius-switch-xs | calc(var(--ods-theme-border-radius) / 2) | 

 |
| --ods-tab-list-border-width | 2px | 

 |
| --ods-tab-margin-horizontal-switch-md | calc(var(--ods-theme-padding-horizontal) / 2) | 

 |
| --ods-tab-margin-horizontal-switch-sm | calc(var(--ods-theme-padding-horizontal) / 2) | 

 |
| --ods-tab-margin-horizontal-switch-xs | calc(var(--ods-theme-padding-horizontal) / 4) | 

 |
| --ods-tab-margin-vertical-switch-md | calc(var(--ods-theme-padding-vertical) / 2) | 

 |
| --ods-tab-margin-vertical-switch-sm | calc(var(--ods-theme-padding-vertical) / 2) | 

 |
| --ods-tab-margin-vertical-switch-xs | calc(var(--ods-theme-padding-vertical) / 4) | 

 |
| --ods-tab-padding-horizontal-md | calc(var(--ods-theme-padding-horizontal) * 1.5) | 

 |
| --ods-tab-padding-horizontal-sm | var(--ods-theme-padding-horizontal) | 

 |
| --ods-tab-padding-horizontal-xs | calc(var(--ods-theme-padding-horizontal) * 0.75) | 

 |
| --ods-tab-padding-vertical-md | var(--ods-theme-padding-horizontal) | 

 |
| --ods-tab-padding-vertical-sm | calc(var(--ods-theme-padding-horizontal) * 0.75) | 

 |
| --ods-tab-padding-vertical-xs | calc(var(--ods-theme-padding-horizontal) / 2) | 

 |
| --ods-tab-text-color-hover | var(--ods-color-primary-500) | 

 |
| --ods-tab-text-color-selected | var(--ods-color-primary-500) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1">
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
    </Tabs>
}
```

### Variant

```jsx
{
  globals: {
    imports: `import { TABS_VARIANT, Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1" variant={TABS_VARIANT.switch}>
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
    </Tabs>
}
```

### Size

```jsx
{
  globals: {
    imports: `import { TABS_SIZE, TABS_VARIANT, Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>MD</p>
      <Tabs defaultValue="tab1" size={TABS_SIZE.md} variant={TABS_VARIANT.switch}>
        <TabList>
          <Tab value="tab1">Tab 1</Tab>
          <Tab value="tab2">Tab 2</Tab>
          <Tab value="tab3">Tab 3</Tab>
        </TabList>
      </Tabs>
      <p>SM</p>
      <Tabs defaultValue="tab1" size={TABS_SIZE.sm} variant={TABS_VARIANT.switch}>
        <TabList>
          <Tab value="tab1">Tab 1</Tab>
          <Tab value="tab2">Tab 2</Tab>
          <Tab value="tab3">Tab 3</Tab>
        </TabList>
      </Tabs>
      <p>XS</p>
      <Tabs defaultValue="tab1" size={TABS_SIZE.xs} variant={TABS_VARIANT.switch}>
        <TabList>
          <Tab value="tab1">Tab 1</Tab>
          <Tab value="tab2">Tab 2</Tab>
          <Tab value="tab3">Tab 3</Tab>
        </TabList>
      </Tabs>
    </>
}
```

### Disabled tab

```jsx
{
  globals: {
    imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1">
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2" disabled>Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
    </Tabs>
}
```

### Horizontal overflow

```jsx
{
  globals: {
    imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    width: '300px'
  }}>
      <Tabs defaultValue="tab1">
        <TabList>
          <Tab value="tab1">Tab 1</Tab>
          <Tab value="tab2">Tab 2</Tab>
          <Tab value="tab3">Tab 3</Tab>
          <Tab value="tab4">Tab 4</Tab>
          <Tab value="tab5">Tab 5</Tab>
          <Tab value="tab6">Tab 6</Tab>
        </TabList>
      </Tabs>
    </div>
}
```

### With content

```jsx
{
  globals: {
    imports: `import { Tabs, TabContent, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1">
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
      <TabContent value="tab1">
        <p>Content 1</p>
      </TabContent>
      <TabContent value="tab2">
        <p>Content 2</p>
      </TabContent>
      <TabContent value="tab3">
        <p>Content 3</p>
      </TabContent>
    </Tabs>
}
```

### Controlled

```jsx
const [value, setValue] = useState('tab1');
  const handleValueChange = (event: TabsValueChangeEvent) => {
    setValue(event.value);
  };
  return <Tabs onValueChange={handleValueChange} value={value}>
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
    </Tabs>;
}
```

### With arrows

```jsx
{
  globals: {
    imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1" withArrows>
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
        <Tab value="tab4">Tab 4</Tab>
        <Tab value="tab5">Tab 5</Tab>
        <Tab value="tab6">Tab 6</Tab>
        <Tab value="tab7">Tab 7</Tab>
        <Tab value="tab8">Tab 8</Tab>
        <Tab value="tab9">Tab 9</Tab>
        <Tab value="tab10">Tab 10</Tab>
        <Tab value="tab11">Tab 11</Tab>
        <Tab value="tab12">Tab 12</Tab>
        <Tab value="tab13">Tab 13</Tab>
        <Tab value="tab14">Tab 14</Tab>
        <Tab value="tab15">Tab 15</Tab>
      </TabList>
    </Tabs>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Tabs

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultValue` | `` | No |  | The initial value of the selected tab. Use when you don't need to control the value of the tabs. |
| `onValueChange` | `` | No |  | Callback fired when the state of selected tab changes. |
| `size` | `` | No | TABS_SIZE.md | The size preset to use. |
| `value` | `` | No |  | The controlled value of the selected tab. |
| `variant` | `` | No | TABS_VARIANT.default | The variant preset to use. |
| `withArrows` | `` | No |  | Whether the component displays navigation arrows around the tabs. |


## Subcomponents


### TabList




### Tab



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `value` | `` | Yes |  | The value of the tab item. |



### TabContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `value` | `` | Yes |  | The value of the tab content item. |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1">
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>

      <TabContent value="tab1">
        <p>Content 1</p>
      </TabContent>
      <TabContent value="tab2">
        <p>Content 2</p>
      </TabContent>
      <TabContent value="tab3">
        <p>Content 3</p>
      </TabContent>
    </Tabs>
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [value, setValue] = useState('tab1');
    const handleValueChange = (event: TabsValueChangeEvent) => {
      setValue(event.value);
    };
    return <Tabs onValueChange={handleValueChange} value={value}>
        <TabList>
          <Tab value="tab1">Tab 1</Tab>
          <Tab value="tab2">Tab 2</Tab>
          <Tab value="tab3">Tab 3</Tab>
        </TabList>
      </Tabs>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1">
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
    </Tabs>
}
```

### Demo

```tsx
{
  render: arg => <Tabs defaultValue="tab1" {...arg}>
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
        <Tab value="tab4">Tab 4</Tab>
        <Tab value="tab5">Tab 5</Tab>
        <Tab value="tab6">Tab 6</Tab>
      </TabList>

      <TabContent value="tab1">
        <p>Tab 1 Content</p>
      </TabContent>

      <TabContent value="tab2">
        <p>Tab 2 Content</p>
      </TabContent>

      <TabContent value="tab3">
        <p>Tab 3 Content</p>
      </TabContent>

      <TabContent value="tab4">
        <p>Tab 4 Content</p>
      </TabContent>

      <TabContent value="tab5">
        <p>Tab 5 Content</p>
      </TabContent>

      <TabContent value="tab6">
        <p>Tab 6 Content</p>
      </TabContent>
    </Tabs>,
  argTypes: orderControls({
    size: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'TABS_SIZE'
        }
      },
      control: {
        type: 'select'
      },
      options: TABS_SIZES
    },
    variant: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'TABS_VARIANT'
        }
      },
      control: {
        type: 'select'
      },
      options: TABS_VARIANTS
    },
    withArrows: {
      table: {
        category: CONTROL_CATEGORY.design
      },
      control: {
        type: 'boolean'
      }
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1">
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2" disabled>Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
    </Tabs>
}
```

### Overflow

```tsx
{
  globals: {
    imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    width: '300px'
  }}>
      <Tabs defaultValue="tab1">
        <TabList>
          <Tab value="tab1">Tab 1</Tab>
          <Tab value="tab2">Tab 2</Tab>
          <Tab value="tab3">Tab 3</Tab>
          <Tab value="tab4">Tab 4</Tab>
          <Tab value="tab5">Tab 5</Tab>
          <Tab value="tab6">Tab 6</Tab>
        </TabList>
      </Tabs>
    </div>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Tabs defaultValue="tab1">
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
    </Tabs>
}
```

### Size

```tsx
{
  globals: {
    imports: `import { TABS_SIZE, TABS_VARIANT, Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <p>MD</p>
      <Tabs defaultValue="tab1" size={TABS_SIZE.md} variant={TABS_VARIANT.switch}>
        <TabList>
          <Tab value="tab1">Tab 1</Tab>
          <Tab value="tab2">Tab 2</Tab>
          <Tab value="tab3">Tab 3</Tab>
        </TabList>
      </Tabs>

      <p>SM</p>
      <Tabs defaultValue="tab1" size={TABS_SIZE.sm} variant={TABS_VARIANT.switch}>
        <TabList>
          <Tab value="tab1">Tab 1</Tab>
          <Tab value="tab2">Tab 2</Tab>
          <Tab value="tab3">Tab 3</Tab>
        </TabList>
      </Tabs>

      <p>XS</p>
      <Tabs defaultValue="tab1" size={TABS_SIZE.xs} variant={TABS_VARIANT.switch}>
        <TabList>
          <Tab value="tab1">Tab 1</Tab>
          <Tab value="tab2">Tab 2</Tab>
          <Tab value="tab3">Tab 3</Tab>
        </TabList>
      </Tabs>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <Tabs withArrows defaultValue="tab1">
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2" disabled>Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
        <Tab value="tab4">Tab 4</Tab>
        <Tab value="tab5">Tab 5</Tab>
        <Tab value="tab6">Tab 6</Tab>
        <Tab value="tab7">Tab 7</Tab>
        <Tab value="tab8">Tab 8</Tab>
        <Tab value="tab9">Tab 9</Tab>
        <Tab value="tab10">Tab 10</Tab>
        <Tab value="tab11">Tab 11</Tab>
        <Tab value="tab12">Tab 12</Tab>
        <Tab value="tab13">Tab 13</Tab>
        <Tab value="tab14">Tab 14</Tab>
        <Tab value="tab15">Tab 15</Tab>
      </TabList>
      <TabContent value="tab1"><p>Content 1</p></TabContent>
      <TabContent value="tab2"><p>Content 2</p></TabContent>
      <TabContent value="tab3"><p>Content 3</p></TabContent>
      <TabContent value="tab4"><p>Content 4</p></TabContent>
      <TabContent value="tab5"><p>Content 5</p></TabContent>
      <TabContent value="tab6"><p>Content 6</p></TabContent>
      <TabContent value="tab7"><p>Content 7</p></TabContent>
      <TabContent value="tab8"><p>Content 8</p></TabContent>
      <TabContent value="tab9"><p>Content 9</p></TabContent>
      <TabContent value="tab10"><p>Content 10</p></TabContent>
      <TabContent value="tab11"><p>Content 11</p></TabContent>
      <TabContent value="tab12"><p>Content 12</p></TabContent>
      <TabContent value="tab13"><p>Content 13</p></TabContent>
      <TabContent value="tab14"><p>Content 14</p></TabContent>
      <TabContent value="tab15"><p>Content 15</p></TabContent>
    </Tabs>
}
```

### Variant

```tsx
{
  globals: {
    imports: `import { TABS_VARIANT, Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1" variant={TABS_VARIANT.switch}>
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
    </Tabs>
}
```

### With Arrows

```tsx
{
  globals: {
    imports: `import { Tabs, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1" withArrows>
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
        <Tab value="tab4">Tab 4</Tab>
        <Tab value="tab5">Tab 5</Tab>
        <Tab value="tab6">Tab 6</Tab>
        <Tab value="tab7">Tab 7</Tab>
        <Tab value="tab8">Tab 8</Tab>
        <Tab value="tab9">Tab 9</Tab>
        <Tab value="tab10">Tab 10</Tab>
        <Tab value="tab11">Tab 11</Tab>
        <Tab value="tab12">Tab 12</Tab>
        <Tab value="tab13">Tab 13</Tab>
        <Tab value="tab14">Tab 14</Tab>
        <Tab value="tab15">Tab 15</Tab>
      </TabList>
    </Tabs>
}
```

### With Content

```tsx
{
  globals: {
    imports: `import { Tabs, TabContent, TabList, Tab } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tabs defaultValue="tab1">
      <TabList>
        <Tab value="tab1">Tab 1</Tab>
        <Tab value="tab2">Tab 2</Tab>
        <Tab value="tab3">Tab 3</Tab>
      </TabList>
      <TabContent value="tab1">
        <p>Content 1</p>
      </TabContent>
      <TabContent value="tab2">
        <p>Content 2</p>
      </TabContent>
      <TabContent value="tab3">
        <p>Content 3</p>
      </TabContent>
    </Tabs>
}
```

## React Components

# Tag

_A **Tag** component is used to display keywords or categories, with a removal feature._

## Overview

---

The **Tag** component is a small UI element used to represent metadata, keywords, categories, or other contextual information.

**Tags** are removable and can include an icon as additional prefix element.

They are commonly used in interfaces for categorizing content, filtering, and adding contextual details.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Tag</td></tr><tr><th scope="row">Also known as</th><td>Chip (previous name), Label, Pill</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=52-11580" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/tag" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-tag--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

Use the **Tag** component to display labels, categories, or metadata that describe or classify content.

**Tags** often appear in contexts like filters and lists.

Ensure that **Tags** are clearly labelled and easily distinguishable from other UI elements.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Tags to represent interactive labels such as filters, categories, or user-defined keywords |
| - Ensure the label is short, descriptive, and scannable, aim for 1–2 words |
| - Use Tags in search contexts, facet filtering, or content classification |
| - Allow Tags to be removable or toggleable if the interaction requires it (e.g., deselecting a filter) |
| - Keep visual distinction between Tags (interactive) and Badges (static) |
| - Trigger removal action when the Tag displays a delete icon |

| ❌ Don't |
| --- |
| - Don't use a Tag if there is no interaction expected, prefer the Badge component instead |
| - Don't use full sentences or long text in a Tag label |
| - Don't mix Tags with different behaviors (e.g., some removable, some static) unless necessary and clearly indicated |
| - Don't use Tags as primary action triggers (e.g., "Submit", "Save"), use Buttons instead |

### Best Practices in Context

1.  **Tag**
2.  **Icon** - optional (left or right)
3.  **Delete icon** - optional
4.  **Label** - optional

## Placement

---

Since it provides extra information to a sibling element, in can be used inside components in various places, referring to the nature of its environment.

Multiple **Tags** can be displayed:

-   on a single line
-   stacked vertically

## Behavior

---

**Tags** can be hovered, focused and disabled.

## Variation

---

### Color

-   **Neutral**: displaying general labels or metadata without conveying any particular status or urgency.
-   **Information** _(default)_: displaying a general information for the user, without conveying any particular urgency.
-   **Success**: indicating positive statuses or successful outcomes, providing a visual cue for achievements or completed tasks.
-   **Warning**: alerting users to potential issues or cautionary information, signaling that attention is needed.
-   **Critical**: highlighting severe issues or urgent information that requires immediate attention, emphasizing high-priority concerns.

### Size

-   **Medium** _(default)_: main size for displaying **Tags**.
-   **Large**: highlighting important labels or metadata with greater visibility and emphasis, making them easily noticeable to users.

## Navigation

---

### Focus Management

The **Tag** component is focusable and follows the same keyboard interactions as the Button component.

### General Keyboard Shortcuts

Pressing Tab moves focus to the **Tag**.

Pressing Enter or Space triggers the assigned action: removing the **Tag**.

Pressing Shift + Tab moves focus to the previous interactive element.

## Accessibility

---

Unlike , **Tags** are interactive and can be removed by the user. To ensure accessibility, it is important to:

-   Group **Tags** properly when they are part of a list.
-   Announce the removal action so screen reader users understand the functionality.

### Structuring groups of Tags with lists

When multiple **Tags** are displayed together (e.g., a collection of selected filters or labels), they should be wrapped in an unordered list of items (`<ul>` and `<li>`) to ensure proper announcement by screen readers.

```jsx
{
  globals: {
    imports: `import { Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ul>
      <li>
        <Tag>Design</Tag>
      </li>
      <li>
        <Tag>Development</Tag>
      </li>
      <li>
        <Tag>Accessibility</Tag>
      </li>
    </ul>
}
```

This allows screen readers to announce a list with the number of items, making it clear that these **Tags** belong to a structured set.

### Alternative approach

When modifying the HTML structure is not possible, use [role="list"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/list_role) and [role="listitem"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/listitem_role) to mimic list semantics.

```jsx
<div role="list">
  <div role="listitem">
    <Tag>
      Design    </Tag>
  </div>
  <div role="listitem">
    <Tag>
      Development    </Tag>
  </div>
  <div role="listitem">
    <Tag>
      Accessibility    </Tag>
  </div>
</div>
```

This ensures that screen readers still recognize the **Tags** as a structured list, even without native `<ul>` and `<li>` elements.

### Announcing the delete action with aria-label

Since the **Tag** is removable, it should provide clear feedback to assistive technologies.

```jsx
{
  globals: {
    imports: `import { Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tag aria-label="Remove my tag">
      My tag    </Tag>
}
```

This ensures that users know the **Tag** can be removed. Screen readers will announce the tag aria label and the button type.

## React Components

# Tag

## Overview

---

## Anatomy

---

Tag

---

## Tag

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes) . |
| 
color

 | `TAG_COLOR` | - | `TAG_COLOR.information` | The color preset to use. |
| 

icon

 | `IconName | null` | - | `ICON_NAME.xmark` | The icon to display on the right side. |
| 

size

 | `TAG_SIZE` | - | `TAG_SIZE.md` | The size preset to use. |

## Enums

---

### TAG_COLOR

-   critical =`"critical"`
-   information =`"information"`
-   neutral =`"neutral"`
-   primary =`"primary"`
-   success =`"success"`
-   warning =`"warning"`

### TAG_SIZE

-   lg =`"lg"`
-   md =`"md"`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-tag-background-color-critical | var(--ods-color-critical-000) | 
 |
| --ods-tag-background-color-critical-active | var(--ods-color-critical-100) | 

 |
| --ods-tag-background-color-critical-hover | var(--ods-color-critical-050) | 

 |
| --ods-tag-background-color-information | var(--ods-color-information-000) | 

 |
| --ods-tag-background-color-information-active | var(--ods-color-information-200) | 

 |
| --ods-tag-background-color-information-hover | var(--ods-color-information-100) | 

 |
| --ods-tag-background-color-neutral | var(--ods-color-neutral-000) | 

 |
| --ods-tag-background-color-neutral-active | var(--ods-color-neutral-200) | 

 |
| --ods-tag-background-color-neutral-hover | var(--ods-color-neutral-100) | 

 |
| --ods-tag-background-color-primary | var(--ods-color-primary-000) | 

 |
| --ods-tag-background-color-primary-active | var(--ods-color-primary-200) | 

 |
| --ods-tag-background-color-primary-hover | var(--ods-color-primary-100) | 

 |
| --ods-tag-background-color-success | var(--ods-color-success-000) | 

 |
| --ods-tag-background-color-success-active | var(--ods-color-success-100) | 

 |
| --ods-tag-background-color-success-hover | var(--ods-color-success-050) | 

 |
| --ods-tag-background-color-warning | var(--ods-color-warning-000) | 

 |
| --ods-tag-background-color-warning-active | var(--ods-color-warning-100) | 

 |
| --ods-tag-background-color-warning-hover | var(--ods-color-warning-050) | 

 |
| --ods-tag-border-color-critical | var(--ods-color-critical-100) | 

 |
| --ods-tag-border-color-critical-active | var(--ods-color-critical-300) | 

 |
| --ods-tag-border-color-critical-hover | var(--ods-color-critical-200) | 

 |
| --ods-tag-border-color-information | var(--ods-color-information-100) | 

 |
| --ods-tag-border-color-information-active | var(--ods-color-information-300) | 

 |
| --ods-tag-border-color-information-hover | var(--ods-color-information-200) | 

 |
| --ods-tag-border-color-neutral | var(--ods-color-neutral-100) | 

 |
| --ods-tag-border-color-neutral-active | var(--ods-color-neutral-300) | 

 |
| --ods-tag-border-color-neutral-hover | var(--ods-color-neutral-200) | 

 |
| --ods-tag-border-color-primary | var(--ods-color-primary-500) | 

 |
| --ods-tag-border-color-primary-active | var(--ods-color-primary-700) | 

 |
| --ods-tag-border-color-primary-hover | var(--ods-color-primary-600) | 

 |
| --ods-tag-border-color-success | var(--ods-color-success-100) | 

 |
| --ods-tag-border-color-success-active | var(--ods-color-success-300) | 

 |
| --ods-tag-border-color-success-hover | var(--ods-color-success-200) | 

 |
| --ods-tag-border-color-warning | var(--ods-color-warning-100) | 

 |
| --ods-tag-border-color-warning-active | var(--ods-color-warning-300) | 

 |
| --ods-tag-border-color-warning-hover | var(--ods-color-warning-200) | 

 |
| --ods-tag-border-width | calc(var(--ods-theme-border-width) * 2) | 

 |
| --ods-tag-column-gap | calc(var(--ods-theme-column-gap) / 2) | 

 |
| --ods-tag-padding-horizontal-lg | var(--ods-theme-padding-horizontal) | 

 |
| --ods-tag-padding-horizontal-md | var(--ods-theme-padding-horizontal) | 

 |
| --ods-tag-padding-vertical-lg | calc(var(--ods-theme-padding-vertical) / 4) | 

 |
| --ods-tag-padding-vertical-md | calc(var(--ods-theme-padding-vertical) / 8) | 

 |
| --ods-tag-row-gap | calc(var(--ods-theme-row-gap) / 2) | 

 |
| --ods-tag-text-color-critical | var(--ods-color-critical-900) | 

 |
| --ods-tag-text-color-critical-active | var(--ods-color-critical-900) | 

 |
| --ods-tag-text-color-critical-hover | var(--ods-color-critical-900) | 

 |
| --ods-tag-text-color-information | var(--ods-color-information-900) | 

 |
| --ods-tag-text-color-information-active | var(--ods-color-information-900) | 

 |
| --ods-tag-text-color-information-hover | var(--ods-color-information-900) | 

 |
| --ods-tag-text-color-neutral | var(--ods-color-neutral-700) | 

 |
| --ods-tag-text-color-neutral-active | var(--ods-color-neutral-700) | 

 |
| --ods-tag-text-color-neutral-hover | var(--ods-color-neutral-700) | 

 |
| --ods-tag-text-color-primary | var(--ods-color-primary-900) | 

 |
| --ods-tag-text-color-primary-active | var(--ods-color-primary-900) | 

 |
| --ods-tag-text-color-primary-hover | var(--ods-color-primary-900) | 

 |
| --ods-tag-text-color-success | var(--ods-color-success-900) | 

 |
| --ods-tag-text-color-success-active | var(--ods-color-success-900) | 

 |
| --ods-tag-text-color-success-hover | var(--ods-color-success-900) | 

 |
| --ods-tag-text-color-warning | var(--ods-color-warning-900) | 

 |
| --ods-tag-text-color-warning-active | var(--ods-color-warning-900) | 

 |
| --ods-tag-text-color-warning-hover | var(--ods-color-warning-900) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Tag>
      My tag    </Tag>
}
```

### Color

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { TAG_COLOR, Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <>
      <Tag color={TAG_COLOR.critical}>Critical</Tag>
      <Tag color={TAG_COLOR.information}>Information</Tag>
      <Tag color={TAG_COLOR.neutral}>Neutral</Tag>
      <Tag color={TAG_COLOR.primary}>Primary</Tag>
      <Tag color={TAG_COLOR.success}>Success</Tag>
      <Tag color={TAG_COLOR.warning}>Warning</Tag>
    </>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Tag disabled>
      My tag    </Tag>
}
```

### Size

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { TAG_SIZE, Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <>
      <Tag size={TAG_SIZE.md}>MD tag</Tag>
      <Tag size={TAG_SIZE.lg}>LG tag</Tag>
    </>
}
```

### Custom icon

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <>
      <Tag icon={null}>
        No Icon      </Tag>
      <Tag icon={ICON_NAME.star}>
        Custom Icon      </Tag>
    </>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Tag

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `color` | `` | No | TAG_COLOR.information | @type=TAG_COLOR The color preset to use. |
| `icon` | `` | No | ICON_NAME.xmark | The icon to display on the right side. |
| `size` | `` | No | TAG_SIZE.md | The size preset to use. |


## Examples


### Accessibility Aria Label

```tsx
{
  globals: {
    imports: `import { Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tag aria-label="Remove my tag">
      My tag
    </Tag>
}
```

### Accessibility Aria Roles

```tsx
{
  globals: {
    imports: `import { Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <div role="list">
      <div role="listitem">
        <Tag>Design</Tag>
      </div>

      <div role="listitem">
        <Tag>Development</Tag>
      </div>

      <div role="listitem">
        <Tag>Accessibility</Tag>
      </div>
    </div>
}
```

### Accessibility List

```tsx
{
  globals: {
    imports: `import { Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <ul>
      <li>
        <Tag>Design</Tag>
      </li>

      <li>
        <Tag>Development</Tag>
      </li>

      <li>
        <Tag>Accessibility</Tag>
      </li>
    </ul>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Tag>
      My tag
    </Tag>
}
```

### Color

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { TAG_COLOR, Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <>
      <Tag color={TAG_COLOR.critical}>Critical</Tag>
      <Tag color={TAG_COLOR.information}>Information</Tag>
      <Tag color={TAG_COLOR.neutral}>Neutral</Tag>
      <Tag color={TAG_COLOR.primary}>Primary</Tag>
      <Tag color={TAG_COLOR.success}>Success</Tag>
      <Tag color={TAG_COLOR.warning}>Warning</Tag>
    </>
}
```

### Custom Icon

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <>
      <Tag icon={null}>
        No Icon
      </Tag>

      <Tag icon={ICON_NAME.star}>
        Custom Icon
      </Tag>
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Tag>
      My tag
    </Tag>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    color: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'TAG_COLOR'
        }
      },
      control: {
        type: 'select'
      },
      options: TAG_COLORS
    },
    children: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'text'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: {
        type: 'boolean'
      }
    },
    icon: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'ICON_NAME'
        }
      },
      control: {
        type: 'select'
      },
      options: ICON_NAMES
    },
    size: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'TAG_SIZE'
        }
      },
      control: {
        type: 'select'
      },
      options: TAG_SIZES
    }
  }),
  args: {
    children: 'My tag'
  }
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Tag disabled>
      My tag
    </Tag>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Tag>
      My tag
    </Tag>
}
```

### Size

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { TAG_SIZE, Tag } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <>
      <Tag size={TAG_SIZE.md}>MD tag</Tag>
      <Tag size={TAG_SIZE.lg}>LG tag</Tag>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexFlow: 'row wrap',
    gap: '8px',
    alignItems: 'center'
  }}>
      <Tag>Default</Tag>
      <Tag disabled>Disabled</Tag>
      <Tag size={TAG_SIZE.lg}>Large</Tag>
      <Tag color={TAG_COLOR.primary}>Primary</Tag>
      <Tag color={TAG_COLOR.success}>Success</Tag>
      <Tag color={TAG_COLOR.warning}>Warning</Tag>
      <Tag color={TAG_COLOR.critical}>Critical</Tag>
      <Tag color={TAG_COLOR.information}>Info</Tag>
      <Tag color={TAG_COLOR.neutral}>Neutral</Tag>
    </div>
}
```

## React Components

# Text

_The **Text** component is used to display and style text content within an application._

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

## Overview

---

The **Text** component is a fundamental UI element for displaying text content.

It supports various presets of styles, sizes, and weights to cater to different textual content requirements such as paragraphs, headings, and inline text.

The component can be extra customized to fit the design and branding needs of the application.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Text</td></tr><tr><th scope="row">Also known as</th><td>Paragraph</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=172-12061" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/text" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-text--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Text** component provides predefined text styles (font size, color, weight, etc.) to ensure visual consistency across the application.

It is ideal when you want to apply a ready-made text style from the OVHcloud Design System without manually setting typography values.

It is not meant for custom typography needs like overriding styles to match a specific heading or layout.

If the available presets don't fit your use case, use the appropriate semantic HTML tag with your own styling instead.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use Text presets to ensure visual and semantic consistency across your UI |
| - Respect a logical heading hierarchy (e.g., h2 follows h1, etc.) for accessibility and SEO |
| - Use Text component to convey meaning and structure, not only appearance |
| - Choose the appropriate preset for readability and emphasis, depending on the context |
| - Ensure sufficient color contrast between text and background for readability and accessibility |

| ❌ Don't |
| --- |
| - Don't skip heading levels (e.g., using heading-3 directly after heading-1), this harms accessibility and user comprehension |
| - Don't override the semantic tag of a preset (e.g., using heading-2 styled as a div) |
| - Don't use Text components just to apply a visual style, consider whether semantic meaning is appropriate |
| - Don't use too many different font sizes and weights on a single view, it creates visual noise and weakens the hierarchy |

## Placement

---

**Text** can be used everywhere in a page as long as levels and heading hierarchy are respected.

It can stand alone or be included in other components depending on the usage.

## Behavior

---

Based on its informational manner, the **Text** default behavior is being read-only.

The user can only select its content if needed.

## Navigation

---

The **Text** component is non-interactive and does not receive keyboard focus. It is purely visual and serves to display textual content without affecting keyboard navigation.

## Accessibility

---

This component complies with the [h1 to h6](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/Heading_Elements) , [p](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/p) , [label](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/label) , [small](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/small) , [span](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span) and [code](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/code) HTML elements.

## React Components

# Text

## Overview

---

## Anatomy

---

Text

---

## Text

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [text elements attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/label#attributes) . |
| 
as

 | `string` | - | `undefined` | Pass a HTML tag you may want to use instead of the preset one. Useful for example to apply heading style to a non-heading element. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the text is displayed in a disabled context. |
| 

preset

 | `TEXT_PRESET` | - | `TEXT_PRESET.paragraph` | The text preset to use. |

## Enums

---

### TEXT_PRESET

-   blockquote =`"blockquote"`
-   caption =`"caption"`
-   code =`"code"`
-   heading1 =`"heading-1"`
-   heading2 =`"heading-2"`
-   heading3 =`"heading-3"`
-   heading4 =`"heading-4"`
-   heading5 =`"heading-5"`
-   heading6 =`"heading-6"`
-   label =`"label"`
-   paragraph =`"paragraph"`
-   small =`"small"`
-   span =`"span"`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Text>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.    </Text>
}
```

### Preset

**Caption** preset can be used should you need ODS caption custom style.

For actual `<caption>` (used in a table) or `<figcaption>` (image), use the native tag using either an OdsText inside of it or the Saas mixin.

```jsx
{
  globals: {
    imports: `import { TEXT_PRESET, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Text preset={TEXT_PRESET.blockquote}>Blockquote</Text>
      <Text preset={TEXT_PRESET.caption}>Caption</Text><br />
      <Text preset={TEXT_PRESET.code}>Code</Text><br />
      <Text preset={TEXT_PRESET.label}>Label</Text>
      <Text preset={TEXT_PRESET.paragraph}>Paragraph</Text>
      <Text preset={TEXT_PRESET.small}>Small</Text><br />
      <Text preset={TEXT_PRESET.span}>Span</Text><br />
      <Text preset={TEXT_PRESET.heading1}>Heading-1</Text>
      <Text preset={TEXT_PRESET.heading2}>Heading-2</Text>
      <Text preset={TEXT_PRESET.heading3}>Heading-3</Text>
      <Text preset={TEXT_PRESET.heading4}>Heading-4</Text>
      <Text preset={TEXT_PRESET.heading5}>Heading-5</Text>
      <Text preset={TEXT_PRESET.heading6}>Heading-6</Text>
    </>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Text disabled>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.    </Text>
}
```

### Caption preset in <table>

```jsx
{
  globals: {
    imports: `import { TEXT_PRESET, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <table style={{
    border: '2px solid rgb(140 140 140)',
    borderCollapse: 'collapse'
  }}>
      <caption style={{
      captionSide: 'bottom'
    }}>
        <Text preset="caption">
          My table title        </Text>
      </caption>
      <thead>
      <tr>
        <th scope="col">Person</th>
        <th scope="col">Age</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <th scope="row">Chris</th>
        <td>22</td>
      </tr>
      </tbody>
    </table>
}
```

### Caption preset in <figcaption>

```jsx
{
  globals: {
    imports: `import { TEXT_PRESET, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <figure>
      <img alt="OVHcloud logo" src="https://images.crunchbase.com/image/upload/c_pad,h_256,w_256,f_auto,q_auto:eco,dpr_1/ayzwkdawmlyzvuummuf4" style={{
      height: '100px'
    }} />
      <figcaption>
        <Text preset={TEXT_PRESET.caption}>
          My picture title        </Text>
      </figcaption>
    </figure>
}
```

### Preset style on a different HTML element

```jsx
{
  globals: {
    imports: `import { TEXT_PRESET, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Text as="span" preset={TEXT_PRESET.heading5}>
      I am a &lt;span&gt; using the heading5 preset style.    </Text>
}
```

## Recipes

---

Chat

Assistant2:59 PM

Welcome to the Chat recipe. Feel free to test the UI behavior by typing anything on your mind below.

Config Tile

VPS 1

4 vCore8 Go RAM100 Go 1 day automated backup Unlimited traffic 200 Mbps

12 months6 monthsNo commitment

From€24.46ex. VAT/monthor €13.19 incl. VAT/month

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Email Field

Email- mandatory

@

.fr.com.dev

The part before the email address (the text before the @) must follow these guidelines:

-   It must end with a letter or a number
-   Allowed special characters are: ".", "_", "-"
-   Special characters cannot be placed next to each other

Feature List

-   Memory: up to 1.5 TB
    
-   SLA: 99.99%
    
-   Guaranteed public bandwidth from 5 Gbps to 25 Gbps
    
-   25 Gbps private bandwidth included
    
-   OVHcloud Link Aggregation
    

Feature List Product Card

WEB HOSTING

NewBest seller

Performance

For demanding online stores and projects.

1 vCore 2,4 GHz, 2 Go RAM1 vCore 2,4 GHz, 4 Go RAM2 vCores 2,4 GHz, 8 Go RAM

From

€24.46ex. VAT/month

or €13.19 incl. VAT/monthfor a 24-month registration

Minimum 2-year registration €100 free with a 5-year registration

Installation fee:Free

-   -   Unlimited websites
        
    -   High power level
        
    -   1 domain name free for the first
        
    -   500 GB SSD storage
        
    -   1,000 email addresses
        
-   1-click CMS
    
    -   WordPress
        
    -   Joomla!
        
    -   Drupal
        
    -   Prestashop
        
-   Database
    
    -   4 x 1 GB databases
        
    -   8 GB Web Cloud Databases
        
-   Security
    
    -   Unlimited free SSL
        
    -   Anti-DDoS protection
        
    -   Anti-virus and anti-spam
        
    -   Daily backups
        
-   Performance
    
    -   99.9% observed availability
        
    -   Guaranteed resources
        
    -   Unlimited traffic
        
    -   Service continuity
        
    -   Boost option to withstand temporary traffic spikes
        
-   Support and additional services
    
    -   Git
        
    -   Standard support
        
    -   SSH access
        
    -   CDN Basic
        

Media Product Card

AI Deploy

Easily deploy machine learning models and applications into production, create your API access points with ease, and make effective predictions.

Status Modal

## React Components/Text

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `` | No |  | @type=string Pass a HTML tag you may want to use instead of the preset one. Useful for example to apply heading style to a non-heading element. |
| `disabled` | `` | No |  | Whether the text is displayed in a disabled context. |
| `preset` | `` | No | TEXT_PRESET.paragraph | The text preset to use. |


## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</Text>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Text>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    </Text>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    children: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    preset: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'TEXT_PRESET'
        }
      },
      control: {
        type: 'select'
      },
      options: TEXT_PRESETS
    }
  }),
  args: {
    children: 'Lorem ipsum dolor sit amet'
  }
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Text disabled>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    </Text>
}
```

### Fig Caption

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <figure>
      <img alt="OVHcloud logo" src="https://images.crunchbase.com/image/upload/c_pad,h_256,w_256,f_auto,q_auto:eco,dpr_1/ayzwkdawmlyzvuummuf4" style={{
      height: '100px'
    }} />

      <figcaption>
        <Text preset={TEXT_PRESET.caption}>
          My picture title
        </Text>
      </figcaption>
    </figure>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</Text>
}
```

### Preset

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Text preset={TEXT_PRESET.blockquote}>Blockquote</Text>
      <Text preset={TEXT_PRESET.caption}>Caption</Text><br />
      <Text preset={TEXT_PRESET.code}>Code</Text><br />
      <Text preset={TEXT_PRESET.label}>Label</Text>
      <Text preset={TEXT_PRESET.paragraph}>Paragraph</Text>
      <Text preset={TEXT_PRESET.small}>Small</Text><br />
      <Text preset={TEXT_PRESET.span}>Span</Text><br />
      <Text preset={TEXT_PRESET.heading1}>Heading-1</Text>
      <Text preset={TEXT_PRESET.heading2}>Heading-2</Text>
      <Text preset={TEXT_PRESET.heading3}>Heading-3</Text>
      <Text preset={TEXT_PRESET.heading4}>Heading-4</Text>
      <Text preset={TEXT_PRESET.heading5}>Heading-5</Text>
      <Text preset={TEXT_PRESET.heading6}>Heading-6</Text>
    </>
}
```

### Reuse Style

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Text as="span" preset={TEXT_PRESET.heading5}>
      I am a &lt;span&gt; using the heading5 preset style.
    </Text>
}
```

### Table Caption

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, Text } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <table style={{
    border: '2px solid rgb(140 140 140)',
    borderCollapse: 'collapse'
  }}>
      <caption style={{
      captionSide: 'bottom'
    }}>
        <Text preset="caption">
          My table title
        </Text>
      </caption>

      <thead>
      <tr>
        <th scope="col">Person</th>
        <th scope="col">Age</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <th scope="row">Chris</th>
        <td>22</td>
      </tr>
      </tbody>
    </table>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div>
      <Text preset={TEXT_PRESET.blockquote}>Blockquote</Text>
      <Text preset={TEXT_PRESET.caption}>Caption</Text><br />
      <Text preset={TEXT_PRESET.code}>Code</Text><br />
      <Text preset={TEXT_PRESET.label}>Label</Text>
      <Text preset={TEXT_PRESET.paragraph}>Paragraph</Text>
      <Text preset={TEXT_PRESET.small}>Small</Text><br />
      <Text preset={TEXT_PRESET.span}>Span</Text><br />
      <Text preset={TEXT_PRESET.heading1}>Heading-1</Text>
      <Text preset={TEXT_PRESET.heading2}>Heading-2</Text>
      <Text preset={TEXT_PRESET.heading3}>Heading-3</Text>
      <Text preset={TEXT_PRESET.heading4}>Heading-4</Text>
      <Text preset={TEXT_PRESET.heading5}>Heading-5</Text>
      <Text preset={TEXT_PRESET.heading6}>Heading-6</Text>
    </div>
}
```

## React Components

# Textarea

_A **Textarea** component allows users to input and edit multiple lines of text._

## Overview

---

The **Textarea** component is used to capture and display multi-line text input from users.

It is typically used in forms where users need to provide detailed information, comments, or descriptions.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Textarea</td></tr><tr><th scope="row">Also known as</th><td>Text Box</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=53-11147" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/textarea" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-textarea--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

Use the **Textarea** component to allow users to enter multi-line text in forms, such as comments, messages, descriptions, or other detailed information.

Ensure that the **Textarea** is appropriately sized for the expected input and provides clear guidance on what information is needed.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Textarea for inputs that may span multiple lines, such as comments, descriptions, or messages |
| - Always pair the Textarea with a label to clearly explain what is expected |
| - Use helper text to provide guidance or clarify formatting when needed |
| - Allow scrolling or resizing if users are likely to enter long text (based on use case) |
| - Use Textarea for a text longer than a single line |

| ❌ Don't |
| --- |
| - Don't use a Textarea for short, single-line inputs. Use an Input instead |
| - Don't rely on a placeholder as a substitute for a proper label since it disappears once the user starts typing |
| - Don't disable resizing if users are expected to write long-form content and might need a larger view |

### Best Practices in Context

1.  **Textarea**
2.  **Placeholder or textarea text**
3.  **Resize handler** - optional

## Placement

---

**Textarea** should be vertically aligned with other form components on a same page.

## Behavior

---

**Textarea** can be hovered, focused, disabled, or set to read-only.

When disabled, the component cannot be focused or interacted with.

If the user types more text than the **Textarea** can display, a scrollbar appears to allow scrolling.

The **Textarea** supports native browser resizing, but it is not enabled by default.

## Navigation

---

### Focus Management

When the **Textarea** is enabled, it can receive focus via keyboard interaction.

A disabled **Textarea** cannot receive focus.

A read-only **Textarea** can receive focus, allowing users to scroll and select content, but its content cannot be modified.

### General Keyboard Shortcuts

Pressing Tab moves focus to the **Textarea**.

Pressing Shift + Tab moves focus to the previous focusable element.

Pressing Enter creates a new line within the **Textarea**.

Pressing Arrow keys moves the caret within the text content.

Pressing Ctrl + Arrow Left / Arrow Right (or Option + Arrow Left / Arrow Right on macOS) moves the caret by word.

Pressing Home / End (or Cmd+ Arrow keys on macOS) moves the caret to the beginning or end of the line.

Pressing Page Up / Page Down scrolls through the content if it's longer than the visible area.

## Accessibility

---

To ensure proper accessibility, it must be correctly labeled, and provide live updates if a character limit is set.

### Always provide an explicit label

Every **Textarea** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Description:

```jsx
<FormField>
  <FormFieldLabel>
    Description:  </FormFieldLabel>
  <Textarea />
</FormField>
```

Screen readers will announce the label and the field.

### Linking helper text

Description:Enter a brief description

```jsx
<FormField>
  <FormFieldLabel>
    Description:  </FormFieldLabel>
  <Textarea />
  <FormFieldHelper>
    Enter a brief description  </FormFieldHelper>
</FormField>
```

Screen readers will announce the label, the field and the helper text.

## React Components

# Textarea

## Overview

---

## Anatomy

---

Textarea

---

## Textarea

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [textarea attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attributes) . |
| 
invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Textarea />
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Textarea disabled />
}
```

### Readonly

```jsx
{
  globals: {
    imports: `import { Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Textarea defaultValue="Readonly" readOnly />
}
```

### Resizable

```jsx
{
  globals: {
    imports: `import { Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Textarea style={{
    resize: 'both'
  }} />
}
```

### Form field

```jsx
const MAX_COUNT = 200;
  const [count, setCount] = useState(0);
  function onInput(e: FormEvent): void {
    setCount((e.target as HTMLTextAreaElement).value.length);
  }
  return <FormField invalid={count > MAX_COUNT}>
      <FormFieldLabel>
        Description:      </FormFieldLabel>
      <Textarea name="description" onInput={onInput} />
      <FormFieldHelper style={{
      display: 'flex',
      justifyContent: 'space-between'
    }}>
        <Text preset={TEXT_PRESET.caption}>
          Helper text        </Text>
        <Text preset={TEXT_PRESET.caption}>
          {count}/{MAX_COUNT}
        </Text>
      </FormFieldHelper>
      <FormFieldError>
        Error message      </FormFieldError>
    </FormField>;
}
```

## Recipes

---

No recipe defined for now.

## React Components/Textarea

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `invalid` | `` | No |  | Whether the component is in error state. |


## Examples


### Accessibility Described By

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldHelper, FormFieldLabel, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Description:
      </FormFieldLabel>

      <Textarea />

      <FormFieldHelper>
        Enter a brief description
      </FormFieldHelper>
    </FormField>
}
```

### Accessibility Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Description:
      </FormFieldLabel>

      <Textarea />
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Textarea placeholder="Textarea" />
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Textarea />
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    cols: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'number'
        }
      },
      control: 'number'
    },
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    placeholder: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'string'
        }
      },
      control: 'text'
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    rows: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'number'
        }
      },
      control: 'number'
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Textarea disabled />
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { TEXT_PRESET, FormField, FormFieldError, FormFieldHelper, FormFieldLabel, Text, Textarea } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const MAX_COUNT = 200;
    const [count, setCount] = useState(0);
    function onInput(e: FormEvent): void {
      setCount((e.target as HTMLTextAreaElement).value.length);
    }
    return <FormField invalid={count > MAX_COUNT}>
        <FormFieldLabel>
          Description:
        </FormFieldLabel>

        <Textarea name="description" onInput={onInput} />

        <FormFieldHelper style={{
        display: 'flex',
        justifyContent: 'space-between'
      }}>
          <Text preset={TEXT_PRESET.caption}>
            Helper text
          </Text>

          <Text preset={TEXT_PRESET.caption}>
            {count}/{MAX_COUNT}
          </Text>
        </FormFieldHelper>

        <FormFieldError>
          Error message
        </FormFieldError>
      </FormField>;
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Textarea placeholder="Textarea" />
}
```

### Read Only

```tsx
{
  globals: {
    imports: `import { Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Textarea defaultValue="Readonly" readOnly />
}
```

### Resizable

```tsx
{
  globals: {
    imports: `import { Textarea } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Textarea style={{
    resize: 'both'
  }} />
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    alignItems: 'flex-start'
  }}>
      <Textarea placeholder="Default" />
      <Textarea disabled placeholder="Disabled" />
      <Textarea invalid placeholder="Invalid" />
      <Textarea readOnly defaultValue="Read only" />
    </div>
}
```

## React Components

# Tile

Checkbox tile

Additional information

## Overview

---

**Tile** is a selectable **Card** used to present options or content blocks that can be selected by the user.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Tile</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/l7NGsGx2CuieJpqHn4B1zo/ODS---UI-Kit?m=auto&amp;node-id=13588-131&amp;t=z9FF7UzVAAvDXO1U-1" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/tile" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

**Tile** is typically used to display a small set of options. Common use cases include:

-   forms
-   surveys
-   preference or configuration screens

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use the Tile component when you need to present a limited number of options to the user, especially when those options require a brief description or additional information |
| - Use a Tile with a checkbox for multiple selections, and with a radio button for single selections |
| - Ensure that the content within Tile is concise and relevant to the user's decision-making process |

| ❌ Don't |
| --- |
| - Avoid using the Tile component for complex or lengthy content, as it may overwhelm the user |
| - Do not use the Tile for options that require a high level of detail or explanation; instead, consider using a different component or providing additional context through Tooltips or Popovers |
| - Refrain from mixing checkbox and radio button in Tiles within the same selection group, as this can cause confusion |

### Best Practices in Context

1.  **Tile**

## Placement

---

The **Tile** component can be used in various layouts, including but not limited to, grids, lists, and as standalone elements within a form. When displaying multiple **Tiles**, ensure they are evenly spaced and aligned to provide a clear and organized user interface. Consider responsive behavior so **Tiles** remain usable and readable on smaller screens.

## Behavior

---

**Tile** component can be selected and disabled.

When selected, **Tile** displays a clear visual indication of its selected state.

When disabled, **Tile** does not respond to user interaction and cannot be selected. All content inside the **Tile** is visually and functionally disabled.

**Tile** can contain any type of content, including interactive elements.

### Navigation

#### Focus Management

**Tile** follows standard keyboard navigation patterns defined by its usage context.

## Accessibility

---

Interactive elements inside **Tile** must follow a logical tab order. Inner interactive content must remain accessible to assistive technologies, without interfering with the **Tile** selection state.

## React Components

# Tile

## Overview

---

## Anatomy

---

Tile

TileAltContainer

---

## Tile

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
disabled

 | `boolean` | - | `false` | Whether the component is disabled. |
| 

selected

 | `boolean` | - | `false` | Whether the component is selected. |

## TileAltContainer

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-tile-alt-container-background-color | var(--ods-color-neutral-050) | 
 |
| --ods-tile-alt-container-background-color-selected | var(--ods-color-primary-075) | 

 |
| --ods-tile-background-color | var(--ods-theme-background-color) | 

 |
| --ods-tile-background-color-disabled | var(--ods-color-neutral-050) | 

 |
| --ods-tile-background-color-selected | var(--ods-color-primary-025) | 

 |
| --ods-tile-border-color | var(--ods-theme-input-border-color) | 

 |
| --ods-tile-border-color-hover | var(--ods-color-neutral-500) | 

 |
| --ods-tile-border-color-selected | var(--ods-color-primary-500) | 

 |
| --ods-tile-border-radius | var(--ods-theme-border-radius) | 

 |
| --ods-tile-border-width | var(--ods-theme-border-width) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Tile } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tile>
      This is a tile content.    </Tile>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Tile } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tile disabled>
      This tile is disabled.    </Tile>
}
```

### Selected

```jsx
{
  globals: {
    imports: `import { Tile } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tile selected>
      This tile is selected.    </Tile>
}
```

### With Checkbox

```jsx
const [selected, setSelected] = useState(false);
  return <Tile selected={selected}>
      <Checkbox checked={selected} onCheckedChange={detail => setSelected(detail.checked === true)}>
        <div style={{
        display: 'flex',
        alignItems: 'center',
        gap: '8px',
        padding: '16px'
      }}>
          <CheckboxControl />
          <CheckboxLabel>Checkbox tile</CheckboxLabel>
        </div>
      </Checkbox>
    </Tile>;
}
```

### With RadioGroup

```jsx
const [selected, setSelected] = useState('radio-1');
  return <RadioGroup value={selected} onValueChange={detail => setSelected(detail.value || '')}>
      <div>
        <Tile selected={selected === 'radio-1'}>
          <Radio value={'radio-1'}>
            <div style={{
            display: 'flex',
            alignItems: 'center',
            gap: '8px',
            padding: '16px'
          }}>
              <RadioControl />
              <RadioLabel>
                Radio 1              </RadioLabel>
            </div>
          </Radio>
        </Tile>
      </div>
      <div>
        <Tile selected={selected === 'radio-2'}>
          <Radio value={'radio-2'}>
            <div style={{
            display: 'flex',
            alignItems: 'center',
            gap: '8px',
            padding: '16px'
          }}>
              <RadioControl />
              <RadioLabel>
                Radio 2              </RadioLabel>
            </div>
          </Radio>
        </Tile>
      </div>
    </RadioGroup>;
}
```

## Recipes

---

Config Tile

VPS 1

4 vCore8 Go RAM100 Go 1 day automated backup Unlimited traffic 200 Mbps

12 months6 monthsNo commitment

From€24.46ex. VAT/monthor €13.19 incl. VAT/month

## React Components/Tile

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `disabled` | `` | No | false | Whether the component is disabled. |
| `selected` | `` | No | false | Whether the component is selected. |


## Subcomponents


### TileAltContainer



## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => {
    const [selected, setSelected] = useState(false);
    return <Tile selected={selected}>
        <Checkbox checked={selected} onCheckedChange={detail => setSelected(detail.checked === true)}>
          <div>
            <div style={{
            display: 'flex',
            alignItems: 'center',
            gap: '8px',
            padding: '16px'
          }}>
              <CheckboxControl />
              <CheckboxLabel>Checkbox tile</CheckboxLabel>
            </div>

            <TileAltContainer style={{
            padding: '16px'
          }}>
              Additional information
            </TileAltContainer>
          </div>
        </Checkbox>
      </Tile>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Tile } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tile>
      This is a tile content.
    </Tile>
}
```

### Demo

```tsx
{
  render: args => <Tile disabled={args.disabled} selected={args.selected} style={{
    padding: '16px',
    width: '300px'
  }}>
      This is a tile content.
    </Tile>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    selected: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Tile } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tile disabled>
      This tile is disabled.
    </Tile>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => {
    const [selected, setSelected] = useState(false);
    return <Tile selected={selected}>
        <Checkbox checked={selected} onCheckedChange={detail => setSelected(detail.checked === true)}>
          <div>
            <div style={{
            display: 'flex',
            alignItems: 'center',
            gap: '8px',
            padding: '16px'
          }}>
              <CheckboxControl />
              <CheckboxLabel>Checkbox tile</CheckboxLabel>
            </div>

            <TileAltContainer style={{
            padding: '16px'
          }}>
              Additional information
            </TileAltContainer>
          </div>
        </Checkbox>
      </Tile>;
  }
}
```

### Selected

```tsx
{
  globals: {
    imports: `import { Tile } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tile selected>
      This tile is selected.
    </Tile>
}
```

### ThemeGenerator

```tsx
{
  name: 'ThemeGenerator',
  parameters: {
    docs: {
      disable: true
    },
    options: {
      showPanel: false
    }
  },
  tags: ['!dev', 'hidden'],
  render: ({}) => <div style={{
    display: 'flex',
    gap: '16px',
    padding: '16px'
  }}>
      <Tile style={{
      padding: '16px',
      width: '200px'
    }}>
        Default tile
      </Tile>
      <Tile selected style={{
      padding: '16px',
      width: '200px'
    }}>
        Selected tile
      </Tile>
      <Tile disabled style={{
      padding: '16px',
      width: '200px'
    }}>
        Disabled tile
      </Tile>
    </div>
}
```

### With Checkbox

```tsx
{
  globals: {
    imports: `import { Tile, Checkbox, CheckboxControl, CheckboxLabel } from '@ovhcloud/ods-react';
    import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [selected, setSelected] = useState(false);
    return <Tile selected={selected}>
        <Checkbox checked={selected} onCheckedChange={detail => setSelected(detail.checked === true)}>
          <div style={{
          display: 'flex',
          alignItems: 'center',
          gap: '8px',
          padding: '16px'
        }}>
            <CheckboxControl />
            <CheckboxLabel>Checkbox tile</CheckboxLabel>
          </div>
        </Checkbox>
      </Tile>;
  }
}
```

### With Radio Group

```tsx
{
  globals: {
    imports: `import { Tile, Radio, RadioGroup, RadioControl, RadioLabel } from '@ovhcloud/ods-react';
    import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [selected, setSelected] = useState('radio-1');
    return <RadioGroup value={selected} onValueChange={detail => setSelected(detail.value || '')}>
        <div>
          <Tile selected={selected === 'radio-1'}>
            <Radio value={'radio-1'}>
              <div style={{
              display: 'flex',
              alignItems: 'center',
              gap: '8px',
              padding: '16px'
            }}>
                <RadioControl />
                <RadioLabel>
                  Radio 1
                </RadioLabel>
              </div>
            </Radio>
          </Tile>
        </div>
        <div>
          <Tile selected={selected === 'radio-2'}>
            <Radio value={'radio-2'}>
              <div style={{
              display: 'flex',
              alignItems: 'center',
              gap: '8px',
              padding: '16px'
            }}>
                <RadioControl />
                <RadioLabel>
                  Radio 2
                </RadioLabel>
              </div>
            </Radio>
          </Tile>
        </div>
      </RadioGroup>;
  }
}
```

## React Components

# Timepicker

_A **Timepicker** is a component that allows users to select a time from a list or set a specific time._

UTC-12UTC-11UTC-10UTC-9UTC-8UTC-7UTC-6UTC-5UTC-4UTC-3UTC-2UTC-1UTC+0UTC+1UTC+2UTC+3UTC+4UTC+5UTC+6UTC+7UTC+8UTC+9UTC+10UTC+11UTC+12

## Overview

---

The **Timepicker** component is used for selecting times in forms and applications. It provides an interface for users to choose a time, ensuring the format is consistent and valid. The component can include features like 12-hour or 24-hour formats, increments, and disabled times.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Timepicker</td></tr><tr><th scope="row">Also known as</th><td>Time</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=53-11662" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/timepicker" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-timepicker--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

**Timepicker** is used to allow a selection of a specific time.

It is useful for scheduling such as planning a meeting.

A **Timepicker** field includes the hour, minutes and optionally seconds. It can be followed by an AM/PM indicator.

For some use case, an optional timezone Select can be added to allow users to select their desired timezone from a list of options.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Timepicker when users need to select a specific time, especially for scheduling or planning scenarios |
| - Always pair the Timepicker with a clear label and/or helper text |
| - Combine the Timepicker with a datepicker when both date and time selection are needed in the same context |
| - When offering timezone selection, provide a restricted and relevant list to avoid overwhelming users. Limit options based on business relevance or geography |

| ❌ Don't |
| --- |
| - Don't use a placeholder as a label since it disappears on input and fails accessibility standards |
| - Don't rely on a placeholder to convey critical information like format or timezone. This should be communicated through helper text or labels |
| - Don't use a Timepicker for ambiguous or unclear time selection use cases. Ensure it is the right fit for your form flow and user intent |
| - Don't expose all global timezones unless necessary |

### Best Practices in Context

1.  **Timepicker**
2.  **Input field**
3.  **Timezones select** - optional

## Placement

---

A **Timepicker** can be used in a page as long as there is a need to allow users to pick a time.

The time field has a fixed width by default but, when used in a form, its width should match the other inputs.

**Timepicker** should be vertically aligned with other form components on a same page.

## Behavior

---

A **Timepicker** can widen to match its container (width of the elements is 50% each).

It has the same states and behaviors for each inner component (i.e.  and ).

## Navigation

---

### Focus Management

When the **Timepicker** is focused, the first focusable segment (typically the hour Input) receives focus automatically.

Focus then moves sequentially through the minute and second inputs (if present), followed by the AM/PM toggle (depending on the browser) and the optional timezone select.

Each individual segment can receive focus and be navigated independently using the keyboard. If a segment (e.g., seconds or timezone) is not visible or disabled, it is skipped in the tab order.

### General Keyboard Shortcuts

Pressing Tab moves focus forward through all focusable segments of the Timepicker:

-   Hour field
-   Minute field
-   Second field (if enabled)
-   AM/PM toggle (if 12-hour format)
-   Time zone Select (if present)

Pressing Shift + Tab moves focus backward through the segments.

When focused on an input segment (hour, minute, second):

-   Typing numeric values replaces the current value
-   Pressing Arrow Up increments the value
-   Pressing Arrow Down decrements the value
-   Pressing Backspace clears the content
-   Pressing Home or fn + Arrow Up increases the value by:
    -   3 units for the hour segment
    -   10 units for minutes and seconds
-   Pressing End or fn + Arrow Down decreases the value by:
    -   3 units for the hour segment
    -   10 units for minutes and seconds

When focused on the AM/PM toggle:

-   Pressing Arrow Up or Arrow Down switches between AM and PM

When focused on the timezone Select, keyboard shortcuts are similar to the Select component:

-   Pressing Space or Arrow Down opens the dropdown
-   Arrow keys navigate options
-   Enter or Tab selects the option and closes the dropdown
-   Pressing Escapecloses the dropdown

## Accessibility

---

To ensure proper accessibility, the **Timepicker** component must be correctly labeled.

### Always provide an explicit label

Every **Timepicker** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either **FormField** or a native label tag.

Starting time:

UTC-12UTC-11UTC-10UTC-9UTC-8UTC-7UTC-6UTC-5UTC-4UTC-3UTC-2UTC-1UTC+0UTC+1UTC+2UTC+3UTC+4UTC+5UTC+6UTC+7UTC+8UTC+9UTC+10UTC+11UTC+12

```jsx
<FormField>
  <FormFieldLabel>
    Starting time:  </FormFieldLabel>
  <Timepicker withSeconds>
    <TimepickerControl />
    <TimepickerTimezoneList />
  </Timepicker>
</FormField>
```

Screen readers will announce the label, the input time and the select field.

## React Components

# Timepicker

## Overview

---

## Anatomy

---

Timepicker

TimepickerControl

TimepickerTimezoneList

---

## Timepicker

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
defaultValue

 | `string` | - | `undefined` | The initial time value. Use when you don't need to control the value of the timepicker. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

i18n

 | `Partial` | - | `undefined` | Internal translations override. |
| 

id

 | `string` | - | `undefined` | The field id. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

locale

 | `LOCALE` | - | `undefined` | The locale used for the translation of the internal elements. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onTimezoneChange

 | `(detail: TimepickerTimezoneChangeDetail) => void` | - | `undefined` | Callback fired when the timezone changes. |
| 

onValueChange

 | `(detail: TimepickerValueChangeDetail) => void` | - | `undefined` | Callback fired when the value changes. |
| 

readOnly

 | `boolean` | - | `undefined` | Whether the component is readonly. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. |
| 

timezone

 | `TIMEZONE` | - | `undefined` | The controlled selected timezone. |
| 

timezones

 | `Timezone[] | TimezonesPreset` | - | `undefined` | A specific or preset list of timezone to display in the selector. |
| 

value

 | `string` | - | `undefined` | The controlled timepicker value. |
| 

withSeconds

 | `boolean` | - | `undefined` | Whether the time input allows seconds selection. |

## TimepickerControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [input attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#attributes) . |

## TimepickerTimezoneList

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |

## Enums

---

### TIMEPICKER_I18N

-   timezoneSelect =`"timepicker.timezone.select"`

### TIMEZONE

-   UTC =`"UTC+0"`
-   UTC1 =`"UTC+1"`
-   UTC10 =`"UTC+10"`
-   UTC11 =`"UTC+11"`
-   UTC12 =`"UTC+12"`
-   UTC2 =`"UTC+2"`
-   UTC3 =`"UTC+3"`
-   UTC4 =`"UTC+4"`
-   UTC5 =`"UTC+5"`
-   UTC6 =`"UTC+6"`
-   UTC7 =`"UTC+7"`
-   UTC8 =`"UTC+8"`
-   UTC9 =`"UTC+9"`
-   UTC_1 =`"UTC-1"`
-   UTC_10 =`"UTC-10"`
-   UTC_11 =`"UTC-11"`
-   UTC_12 =`"UTC-12"`
-   UTC_2 =`"UTC-2"`
-   UTC_3 =`"UTC-3"`
-   UTC_4 =`"UTC-4"`
-   UTC_5 =`"UTC-5"`
-   UTC_6 =`"UTC-6"`
-   UTC_7 =`"UTC-7"`
-   UTC_8 =`"UTC-8"`
-   UTC_9 =`"UTC-9"`

### TIMEZONES_PRESET

-   all =`"all"`

## Interfaces

---

### TimepickerTimezoneChangeDetail

-   `value: TIMEZONE`

### TimepickerValueChangeDetail

-   `timezone?: TIMEZONE`
-   `value: string`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Timepicker, TimepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Timepicker>
      <TimepickerControl />
    </Timepicker>
}
```

### Disabled

UTC-12UTC-11UTC-10UTC-9UTC-8UTC-7UTC-6UTC-5UTC-4UTC-3UTC-2UTC-1UTC+0UTC+1UTC+2UTC+3UTC+4UTC+5UTC+6UTC+7UTC+8UTC+9UTC+10UTC+11UTC+12

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Timepicker disabled>
        <TimepickerControl />
      </Timepicker>
      <Timepicker disabled>
        <TimepickerControl />
        <TimepickerTimezoneList />
      </Timepicker>
    </>
}
```

### Readonly

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Timepicker readOnly>
        <TimepickerControl />
      </Timepicker>
      <Timepicker readOnly>
        <TimepickerControl />
        <TimepickerTimezoneList />
      </Timepicker>
    </>
}
```

### With seconds

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Timepicker withSeconds>
        <TimepickerControl />
      </Timepicker>
      <Timepicker withSeconds>
        <TimepickerControl />
        <TimepickerTimezoneList />
      </Timepicker>
    </>
}
```

### Timezone list

You can use update the list of timezone to select from.

By default, the list use the `all` preset that displays all supported timezones. But you can pass a specific subset to limit the possible choices.

For further explanation about timezone strategy, see .

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>All timezones</span>
      <Timepicker>
        <TimepickerControl />
        <TimepickerTimezoneList />
      </Timepicker>
      <span>Subset of timezone</span>
      <Timepicker timezones={['UTC-10', 'UTC+0', 'UTC+10']}>
        <TimepickerControl />
        <TimepickerTimezoneList />
      </Timepicker>
    </>
}
```

### Form field

```jsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Timepicker:      </FormFieldLabel>
      <Timepicker>
        <TimepickerControl />
      </Timepicker>
    </FormField>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Timepicker

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultValue` | `` | No |  | The initial time value. Use when you don't need to control the value of the timepicker. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `i18n` | `` | No |  | Internal translations override. |
| `id` | `` | No |  | The field id. |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `locale` | `` | No |  | The locale used for the translation of the internal elements. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onTimezoneChange` | `` | No |  | Callback fired when the timezone changes. |
| `onValueChange` | `` | No |  | Callback fired when the value changes. |
| `readOnly` | `` | No |  | Whether the component is readonly. |
| `required` | `` | No |  | Whether the component is required. |
| `timezone` | `` | No |  | The controlled selected timezone. |
| `timezones` | `` | No |  | A specific or preset list of timezone to display in the selector. |
| `value` | `` | No |  | The controlled timepicker value. |
| `withSeconds` | `` | No |  | Whether the time input allows seconds selection. |


## Subcomponents


### TimepickerControl




### TimepickerTimezoneList



## Examples


### Accessibility Label

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Starting time:
      </FormFieldLabel>

      <Timepicker withSeconds>
        <TimepickerControl />

        <TimepickerTimezoneList />
      </Timepicker>
    </FormField>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Timepicker defaultValue="12:00">
      <TimepickerControl />

      <TimepickerTimezoneList />
    </Timepicker>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Timepicker, TimepickerControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Timepicker>
      <TimepickerControl />
    </Timepicker>
}
```

### Demo

```tsx
{
  render: (arg: DemoArg) => <Timepicker disabled={arg.disabled} invalid={arg.invalid} readOnly={arg.readOnly} withSeconds={arg.withSeconds}>
      <TimepickerControl />

      {arg.withTimezones && <TimepickerTimezoneList />}
    </Timepicker>,
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: {
        type: 'boolean'
      }
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    readOnly: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    withSeconds: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    },
    withTimezones: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'boolean'
        }
      },
      control: 'boolean'
    }
  })
}
```

### Disabled

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Timepicker disabled>
        <TimepickerControl />
      </Timepicker>

      <Timepicker disabled>
        <TimepickerControl />

        <TimepickerTimezoneList />
      </Timepicker>
    </>
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <FormField>
      <FormFieldLabel>
        Timepicker:
      </FormFieldLabel>

      <Timepicker>
        <TimepickerControl />
      </Timepicker>
    </FormField>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Timepicker defaultValue="12:00">
      <TimepickerControl />

      <TimepickerTimezoneList />
    </Timepicker>
}
```

### Readonly

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Timepicker readOnly>
        <TimepickerControl />
      </Timepicker>

      <Timepicker readOnly>
        <TimepickerControl />

        <TimepickerTimezoneList />
      </Timepicker>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    alignItems: 'flex-start'
  }}>
      <Timepicker>
        <TimepickerControl />
      </Timepicker>

      <Timepicker>
        <TimepickerControl />
        <TimepickerTimezoneList />
      </Timepicker>

      <Timepicker disabled>
        <TimepickerControl />
      </Timepicker>

      <Timepicker readOnly>
        <TimepickerControl />
      </Timepicker>
    </div>
}
```

### Timezone List

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>All timezones</span>

      <Timepicker>
        <TimepickerControl />

        <TimepickerTimezoneList />
      </Timepicker>

      <span>Subset of timezone</span>

      <Timepicker timezones={['UTC-10', 'UTC+0', 'UTC+10']}>
        <TimepickerControl />

        <TimepickerTimezoneList />
      </Timepicker>
    </>
}
```

### With Seconds

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { Timepicker, TimepickerControl, TimepickerTimezoneList } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Timepicker withSeconds>
        <TimepickerControl />
      </Timepicker>

      <Timepicker withSeconds>
        <TimepickerControl />

        <TimepickerTimezoneList />
      </Timepicker>
    </>
}
```

## React Components

# Toaster

_**Toaster** component provides a section on the page where you'll be able to display **Toast** to notify the user._

## Overview

---

A **Toast** is a non-blocking notification used to communicate brief messages to the user.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Toaster</td></tr><tr><th scope="row">Also known as</th><td>Notification, Toast</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/9bnlxD8FSlsM1AkmxZbNLG/ODS---UI-Kit?node-id=11140-2948&amp;p=f&amp;t=Rrmj84Hj95VYn82V-0" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/toaster" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Toast** is used for short-lived messages that do not demand immediate user action. They are commonly used for:

-   confirmations (e.g., "Changes saved").
-   warnings or errors (e.g., "Connection lost").
-   informational updates (e.g., "New message received").

### Message vs Toast

**Message**

-   Persistent or semi-persistent information banner.
-   Usually appears inline or at the top of a page as part of the content flow.
-   Designed for status communication (success, warning, critical error, info) that’s tied to a specific screen or context.
-   Remains visible until dismissed or until the state changes.

**Toast**

-   Ephemeral notification that appears for a short time (usually overlay in a corner of the viewport)
-   Designed for lightweight, transient feedback ("Action succeeded", "File uploaded"), except for toast with actions.
-   Auto-dismisses after a few seconds.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Toast to inform users of non-critical system events or confirmations |
| - Keep messages short and clear |
| - Queue multiple Toasts when triggered in quick succession |

| ❌ Don't |
| --- |
| - Use Toasts for critical alerts that require immediate user attention. Use Modal instead |
| - Overuse Toasts for every user action |
| - Allow Toasts to hide critical UI elements |
| - Display long content or actions that require extended attention in a Toast |

### Best Practices in Context

1.  **Toast**
2.  **Icon** - optional
3.  **Close button** - optional
4.  **Content**

## Placement

---

**Toasts** are typically positioned at one of the corners of the viewport (default is top-end), but the placement is configurable to match the application's UX needs.

## Behavior

---

### Auto-dismiss

By default, a **Toast*** is ephemeral and disappears automatically after a predefined duration (default is 3 seconds).

The auto-dismiss timer is paused when the user hovers over the **Toast**, and resumes once the hover ends.

The **Toast** may include a close button, allowing the user to dismiss it before the timer expires.

### Persistent Toast

**Toasts** can be configured to remain visible until the user dismisses them or completes the associated action.

In this case, a close button is always required.

### Manual dismiss

A close button is displayed for manual dismissal.

Manual dismiss is compatible with both auto-dismiss and persistent **Toasts**.

### Stacking

**Toasts** stack within their display area, with a default maximum of 3 visible **Toasts**.

The stack follows a FIFO (First In, First Out) order, the most recent **Toast** appears first.

When a **Toast** is dismissed (automatically or manually), the remaining **Toasts** shift to fill the gap, preserving their relative order.

Stacking direction depends on placement:

-   top placements: the newest **Toast** appears at the top of the stack, pushing older ones downward.
-   bottom placements: the newest **Toast** appears at the bottom of the stack, pushing older ones upward.

## Variation

---

### Color

-   **Information** _(default)_: informing users of specific content, providing global feedback to the user.
-   **Success**: confirming that an action has been completed successfully, providing positive feedback to the user.
-   **Warning**: alerting users to potential issues or cautionary information, prompting them to take preventive actions.
-   **Critical**: highlighting severe and potentially catastrophic issues that demand urgent and decisive action to prevent significant negative consequences.

## Navigation

---

**Toasts** interactive elements can receive keyboard focus.

**Toasts** do not trap focus nor interfere with the active workflow.

## Accessibility

---

The **Toaster** component handles by itself the accessibility requirements.

## React Components

# Toaster

## Overview

---

## Toaster

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. |
| 

dismissible

 | `boolean` | - | `undefined` | Whether toasts can be manually removed. |
| 

duration

 | `number` | - | `3000` | Toast duration before being removed. |
| 

max

 | `number` | - | `3` | The maximum number of toast displayed at the same time. |
| 

position

 | `TOASTER_POSITION` | - | `TOASTER_POSITION.topEnd` | The position on screen where the toasts will appear. |

## Enums

---

### TOASTER_POSITION

-   bottom =`"bottom"`
-   bottomEnd =`"bottom-end"`
-   bottomStart =`"bottom-start"`
-   top =`"top"`
-   topEnd =`"top-end"`
-   topStart =`"top-start"`

### TOAST_COLOR

-   critical =`"critical"`
-   information =`"information"`
-   neutral =`"neutral"`
-   primary =`"primary"`
-   success =`"success"`
-   warning =`"warning"`

## Interfaces

---

### ToastOption

-   `className?: string`
-   `dismissible?: boolean`
-   `duration?: number`
-   `icon?: IconName`
-   `id?: string`
-   `toasterId?: string`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-toaster-padding | 24px | 
 |

## How to use?

---

1.  Add the `<Toaster />` component somewhere in your app.
2.  Import the `toast` function.
3.  Trigger toasts using `toast` functions.

### Triggering toast

The `toast` exposes the following trigger methods:

-   itself as a function to trigger a default toast.
-   `critical`: to trigger a critical toast.
-   `information`: to trigger an information toast.
-   `neutral`: to trigger a neutral toast.
-   `primary`: to trigger a primary toast.
-   `success`: to trigger a success toast.
-   `warning`: to trigger a warning toast.

Each trigger method implements the following interface: `(content: ReactNode, option?: ToastOption) => string` and will return the toast `id`.

By default the toast will apply the configuration of the `Toaster` component (dismissible, duration, ...). But you can override this setting per toast using `ToastOption` (check examples beneath for more details).

### Removing toast

By default, a toast will disappear after its duration, or if dismissible, on the close button click.

Though you can also programmatically remove a specific toast using the `toast` remove method: `remove: (toasterId: string) => void`.

### Managing multiple Toaster

If you want to manage multiple toasters, you'll have to set an `id` to each and set the `toasterId` when triggering the toast.

```typescript
import { TOASTER_POSITION, Toaster, toast } from '@ovhcloud/ods-react';
const MyApp = () => (
  <>
    <Toaster id="notifications" />
    <Toaster id="alerts" position={ TOASTER_POSITION.top } />
    <button onClick={ () => toast('Some notification', { toasterId: 'notifications' }) }>
      Trigger notification    </button>
    <button onClick={ () => toast.critical('Some alert', { toasterId: 'alerts' }) }>
      Trigger alert    </button>
  </>
);
```

## Examples

---

### Default

```jsx
<>
  <Toaster />
  <Button onClick={() => toast('Notification message')}>
    Trigger toast  </Button>
</>
```

### Colors

```jsx
<>
  <Toaster id="colors" />
  <div style={{
  display: 'flex',
  gap: '8px',
  flexWrap: 'wrap'
}}>
    <Button color={BUTTON_COLOR.critical} onClick={() => toast.critical('Critical', {
    toasterId: 'colors'
  })}>
      Critical toast    </Button>
    <Button color={BUTTON_COLOR.information} onClick={() => toast.information('Information', {
    toasterId: 'colors'
  })}>
      Information toast    </Button>
    <Button color={BUTTON_COLOR.neutral} onClick={() => toast.neutral('Neutral', {
    toasterId: 'colors'
  })}>
      Neutral toast    </Button>
    <Button color={BUTTON_COLOR.primary} onClick={() => toast.primary('Primary', {
    toasterId: 'colors'
  })}>
      Primary toast    </Button>
    <Button color={BUTTON_COLOR.success} onClick={() => toast.success('Success', {
    toasterId: 'colors'
  })}>
      Success toast    </Button>
    <Button color={BUTTON_COLOR.warning} onClick={() => toast.warning('Warning', {
    toasterId: 'colors'
  })}>
      Warning toast    </Button>
  </div>
</>
```

### Dismissible

```jsx
<>
  <Toaster dismissible={false} id="dismissible" />
  <div style={{
  display: 'flex',
  gap: '8px',
  flexWrap: 'wrap'
}}>
    <Button onClick={() => toast('Non dismissible toast', {
    toasterId: 'dismissible'
  })}>
      Trigger non dismissible toast    </Button>
    <Button onClick={() => toast('Dismissible toast', {
    dismissible: true,
    toasterId: 'dismissible'
  })}>
      Trigger dismissible toast    </Button>
  </div>
</>
```

### Duration

```jsx
<>
  <Toaster duration={Infinity} id="duration" />
  <div style={{
  display: 'flex',
  gap: '8px',
  flexWrap: 'wrap'
}}>
    <Button onClick={() => toast('Infinite toast', {
    toasterId: 'duration'
  })}>
      Trigger infinite toast    </Button>
    <Button onClick={() => toast('3 seconds toast', {
    duration: 3000,
    toasterId: 'duration'
  })}>
      Trigger 3 seconds toast    </Button>
  </div>
</>
```

### Icon

```jsx
<>
  <Toaster id="icon" />
  <Button onClick={() => toast('Notification message with icon', {
  icon: ICON_NAME.circleInfo,
  toasterId: 'icon'
})}>
    Trigger toast  </Button>
</>
```

### Position

```jsx
{
  globals: {
    imports: `import { ICON_NAME, TOASTER_POSITION, Button, Toaster, toast } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Toaster id="top-start" position={TOASTER_POSITION.topStart} />
      <Toaster id="top" position={TOASTER_POSITION.top} />
      <Toaster id="top-end" position={TOASTER_POSITION.topEnd} />
      <Toaster id="bottom-start" position={TOASTER_POSITION.bottomStart} />
      <Toaster id="bottom" position={TOASTER_POSITION.bottom} />
      <Toaster id="bottom-end" position={TOASTER_POSITION.bottomEnd} />
      <div style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(3, 1fr)',
      gridTemplateRows: 'repeat(2, 1fr)',
      gap: '20px'
    }}>
        <Button onClick={() => toast('Top Start', {
        toasterId: 'top-start'
      })}>
          Top Start        </Button>
        <Button onClick={() => toast('Top', {
        toasterId: 'top'
      })}>
          Top        </Button>
        <Button onClick={() => toast('Top End', {
        toasterId: 'top-end'
      })}>
          Top End        </Button>
        <Button onClick={() => toast('Bottom Start', {
        toasterId: 'bottom-start'
      })}>
          Bottom Start        </Button>
        <Button onClick={() => toast('Bottom', {
        toasterId: 'bottom'
      })}>
          Bottom        </Button>
        <Button onClick={() => toast('Bottom End', {
        toasterId: 'bottom-end'
      })}>
          Bottom End        </Button>
      </div>
    </>
}
```

### Custom Content

```jsx
<>
  <Toaster id="custom-content" />
  <Button onClick={() => toast(<div>
        <Text preset={TEXT_PRESET.label}>
          Toast title        </Text>
        <Text>
          Toast text helps users providing feedback or information.        </Text>
        <Link>
          Some Link        </Link>
      </div>, {
  icon: ICON_NAME.circleInfo,
  toasterId: 'custom-content'
})}>
    Trigger toast  </Button>
</>
```

## Recipes

---

No recipe defined for now.

## React Components/Toaster

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. |
| `dismissible` | `` | No |  | Whether toasts can be manually removed. |
| `duration` | `` | No | 3000 | Toast duration before being removed. |
| `max` | `` | No | 3 | The maximum number of toast displayed at the same time. |
| `position` | `` | No | TOASTER_POSITION.topEnd | The position on screen where the toasts will appear. |


## Examples


### Colors

```tsx
{
  globals: {
    imports: `import { BUTTON_COLOR, Button, Toaster, toast } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Toaster id="colors" />

      <div style={{
      display: 'flex',
      gap: '8px',
      flexWrap: 'wrap'
    }}>
        <Button color={BUTTON_COLOR.critical} onClick={() => toast.critical('Critical', {
        toasterId: 'colors'
      })}>
          Critical toast
        </Button>

        <Button color={BUTTON_COLOR.information} onClick={() => toast.information('Information', {
        toasterId: 'colors'
      })}>
          Information toast
        </Button>

        <Button color={BUTTON_COLOR.neutral} onClick={() => toast.neutral('Neutral', {
        toasterId: 'colors'
      })}>
          Neutral toast
        </Button>

        <Button color={BUTTON_COLOR.primary} onClick={() => toast.primary('Primary', {
        toasterId: 'colors'
      })}>
          Primary toast
        </Button>

        <Button color={BUTTON_COLOR.success} onClick={() => toast.success('Success', {
        toasterId: 'colors'
      })}>
          Success toast
        </Button>

        <Button color={BUTTON_COLOR.warning} onClick={() => toast.warning('Warning', {
        toasterId: 'colors'
      })}>
          Warning toast
        </Button>
      </div>
    </>
}
```

### Custom Content

```tsx
{
  globals: {
    imports: `import { ICON_NAME, TEXT_PRESET, Button, Link, Text, Toaster, toast } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Toaster id="custom-content" />

      <Button onClick={() => toast(<div>
            <Text preset={TEXT_PRESET.label}>
              Toast title
            </Text>

            <Text>
              Toast text helps users providing feedback or information.
            </Text>

            <Link>
              Some Link
            </Link>
          </div>, {
      icon: ICON_NAME.circleInfo,
      toasterId: 'custom-content'
    })}>
        Trigger toast
      </Button>
    </>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Button, Toaster, toast } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Toaster />

      <Button onClick={() => toast('Notification message')}>
        Trigger toast
      </Button>
    </>
}
```

### Demo

```tsx
{
  render: arg => <>
      <Toaster {...arg} />

      <Button onClick={() => toast('Notification message')}>
        Trigger toast
      </Button>
    </>,
  argTypes: orderControls({
    dismissible: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    duration: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'number'
    },
    position: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'TOASTER_POSITION'
        }
      },
      control: {
        type: 'select'
      },
      options: TOASTER_POSITIONS
    },
    max: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'number'
    }
  })
}
```

### Dismissible

```tsx
{
  globals: {
    imports: `import { Button, Toaster, toast } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Toaster dismissible={false} id="dismissible" />

      <div style={{
      display: 'flex',
      gap: '8px',
      flexWrap: 'wrap'
    }}>
        <Button onClick={() => toast('Non dismissible toast', {
        toasterId: 'dismissible'
      })}>
          Trigger non dismissible toast
        </Button>

        <Button onClick={() => toast('Dismissible toast', {
        dismissible: true,
        toasterId: 'dismissible'
      })}>
          Trigger dismissible toast
        </Button>
      </div>
    </>
}
```

### Duration

```tsx
{
  globals: {
    imports: `import { Button, Toaster, toast } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Toaster duration={Infinity} id="duration" />

      <div style={{
      display: 'flex',
      gap: '8px',
      flexWrap: 'wrap'
    }}>
        <Button onClick={() => toast('Infinite toast', {
        toasterId: 'duration'
      })}>
          Trigger infinite toast
        </Button>

        <Button onClick={() => toast('3 seconds toast', {
        duration: 3000,
        toasterId: 'duration'
      })}>
          Trigger 3 seconds toast
        </Button>
      </div>
    </>
}
```

### Icon

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Button, Toaster, toast } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Toaster id="icon" />

      <Button onClick={() => toast('Notification message with icon', {
      icon: ICON_NAME.circleInfo,
      toasterId: 'icon'
    })}>
        Trigger toast
      </Button>
    </>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => {
    const [count, setCount] = useState(1);
    function createToast(): void {
      toast(`Toast message ${count}`, {
        toasterId: 'overview'
      });
      setCount(c => c + 1);
    }
    return <>
        <Toaster id="overview" />

        <Button onClick={createToast}>
          Trigger toast
        </Button>
      </>;
  }
}
```

### Position

```tsx
{
  globals: {
    imports: `import { ICON_NAME, TOASTER_POSITION, Button, Toaster, toast } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Toaster id="top-start" position={TOASTER_POSITION.topStart} />
      <Toaster id="top" position={TOASTER_POSITION.top} />
      <Toaster id="top-end" position={TOASTER_POSITION.topEnd} />
      <Toaster id="bottom-start" position={TOASTER_POSITION.bottomStart} />
      <Toaster id="bottom" position={TOASTER_POSITION.bottom} />
      <Toaster id="bottom-end" position={TOASTER_POSITION.bottomEnd} />

      <div style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(3, 1fr)',
      gridTemplateRows: 'repeat(2, 1fr)',
      gap: '20px'
    }}>
        <Button onClick={() => toast('Top Start', {
        toasterId: 'top-start'
      })}>
          Top Start
        </Button>

        <Button onClick={() => toast('Top', {
        toasterId: 'top'
      })}>
          Top
        </Button>

        <Button onClick={() => toast('Top End', {
        toasterId: 'top-end'
      })}>
          Top End
        </Button>

        <Button onClick={() => toast('Bottom Start', {
        toasterId: 'bottom-start'
      })}>
          Bottom Start
        </Button>

        <Button onClick={() => toast('Bottom', {
        toasterId: 'bottom'
      })}>
          Bottom
        </Button>

        <Button onClick={() => toast('Bottom End', {
        toasterId: 'bottom-end'
      })}>
          Bottom End
        </Button>
      </div>
    </>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Toaster duration={Infinity} max={Infinity} />

      <div style={{
      display: 'flex',
      gap: '8px',
      flexWrap: 'wrap'
    }}>
        <Button color={BUTTON_COLOR.critical} onClick={() => toast.critical('Critical', {
        icon: ICON_NAME.circleXmark
      })}>
          Critical toast
        </Button>

        <Button color={BUTTON_COLOR.information} onClick={() => toast.information('Information', {
        icon: ICON_NAME.circleInfo
      })}>
          Information toast
        </Button>

        <Button color={BUTTON_COLOR.neutral} onClick={() => toast.neutral('Neutral', {
        icon: ICON_NAME.email
      })}>
          Neutral toast
        </Button>

        <Button color={BUTTON_COLOR.primary} onClick={() => toast.primary('Primary', {
        icon: ICON_NAME.lightbulb
      })}>
          Primary toast
        </Button>

        <Button color={BUTTON_COLOR.success} onClick={() => toast.success('Success', {
        icon: ICON_NAME.circleCheck
      })}>
          Success toast
        </Button>

        <Button color={BUTTON_COLOR.warning} onClick={() => toast.warning('Warning', {
        icon: ICON_NAME.triangleExclamation
      })}>
          Warning toast
        </Button>
      </div>
    </>
}
```

## React Components

# Toggle

## Overview

---

The **Toggle** component is used to enable or disable a state, setting or feature.

It provides a clear visual indication of the current state and allows users to change states with a single click or tap.

**Toggles** are commonly used in settings, preferences, and forms where a binary choice is required.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Toggle</td></tr><tr><th scope="row">Also known as</th><td>Toggle Switch, Switch</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=53-15319" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/toggle" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-form-elements-toggle--documentation" target="_blank">Previous major version <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2" data-ods="link" href="#">Form Guidelines</a></td></tr></tbody></table>

## Usage

---

A **Toggle** is commonly used for binary choice.

The user can decide to switch between two states with an immediate effect visible at a glance.

It can be used in following use cases when you need to allow the user to:

-   turn an option or settings on or off
-   add or remove an item or an option

A **Toggle** should never require users to press a button to apply their settings (if a setting requires a button, use a checkbox instead).

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Toggle to switch between two states when the change has an immediate effect (e.g., turning a setting on or off) |
| - Ensure the Toggle label clearly describes the "on" state, so users understand what enabling it will do |
| - Place any related elements (e.g., changes triggered by the Toggle) close to the Toggle to reinforce the connection visually |
| - Use Toggle in mobile-friendly settings where quick, touch-based interactions are expected |
| - Use Toggle in forms only when the change takes effect immediately and does not require submission to apply |

| ❌ Don't |
| --- |
| - Don't use a Toggle for changes that only take effect after form submission. Use a Checkbox instead |
| - Don't use ambiguous or overly long labels |
| - Don't use Toggle for multi-state or complex options since it is meant for binary (on/off) interactions only |

### Best Practices in Context

1.  **Toggle**
2.  **Handle**
3.  **Track**
4.  **State label** - optional

## Placement

---

Place the **Toggle** component near the setting or feature it controls.

It should be positioned within forms, settings panels, or any relevant context where a binary choice is required.

Ensure it is easily accessible and visible.

## Behavior

---

When the user clicks on a **Toggle**, it has an immediate effect:

-   set to "on" position, the thumb will slide to the right of the track
-   set to "off" position, the thumb will slide to the left of the track

## Navigation

---

### Focus Management

When the **Toggle** component is focused, focus is automatically set to the **Toggle** itself.

A disabled **Toggle** cannot receive focus and cannot be toggled.

### General Keyboard Shortcuts

Pressing Tab moves focus to the **Toggle** component.

Pressing Shift + Tab moves focus backward to the previous focusable element.

When focused, pressing Space or Enter immediately changes the **Toggle**'s state from on to off or off to on.

## Accessibility

---

This component complies with the [Checkbox WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox) .

### Always provide an explicit label

Every **Toggle** must have a clear and explicit label to ensure that users (especially screen reader users) understand its purpose, using either a **ToggleLabel** or an [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label) for context.

Enable dark mode

```jsx
{
  globals: {
    imports: `import { Toggle, ToggleControl, ToggleLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle>
      <ToggleControl />
      <ToggleLabel>
        Enable dark mode      </ToggleLabel>
    </Toggle>
}
```

Screen readers will announce a checkbox, its state and the label.

Dark mode

```jsx
<Toggle aria-label="Enable dark mode">
  <ToggleControl />
  <ToggleLabel>
    Dark mode  </ToggleLabel>
</Toggle>
```

Screen readers will announce a checkbox, its state and the label.

## React Components

# Toggle

## Overview

---

## Anatomy

---

Toggle

ToggleControl

ToggleLabel

---

Toggle label

## Toggle

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [label attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/label#attributes) . |
| 
checked

 | `boolean` | - | `undefined` | The controlled checked state of the toggle. |
| 

defaultChecked

 | `boolean` | - | `undefined` | The initial checked state of the toggle. Use when you don't need to control the checked state of the toggle. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

invalid

 | `boolean` | - | `undefined` | Whether the component is in error state. |
| 

name

 | `string` | - | `undefined` | The name of the form element. Useful for form submission. |
| 

onCheckedChange

 | `(detail: ToggleCheckedChangeDetail) => void` | - | `undefined` | Callback fired when the checked state changes. |
| 

required

 | `boolean` | - | `undefined` | Whether the component is required. |
| 

value

 | `string` | - | `undefined` | The value of form element. Useful for form submission. |
| 

withLabels

 | `boolean` | - | `undefined` | Whether the component displays "ON/OFF" labels. |

## ToggleControl

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |

## ToggleLabel

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [span attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/span#attributes) . |

## Interfaces

---

### ToggleCheckedChangeDetail

-   `checked: boolean`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-toggle-control-background-color | var(--ods-color-neutral-200) | 
 |
| --ods-toggle-control-background-color-hover | var(--ods-color-neutral-300) | 

 |
| --ods-toggle-control-border-color | var(--ods-color-neutral-200) | 

 |
| --ods-toggle-control-border-color-hover | var(--ods-color-neutral-300) | 

 |
| --ods-toggle-control-label-off-text-color | var(--ods-color-neutral-700) | 

 |
| --ods-toggle-control-label-on-text-color | var(--ods-color-neutral-000) | 

 |
| --ods-toggle-control-thumb-background-color | var(--ods-color-neutral-000) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Toggle, ToggleControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle>
      <ToggleControl />
    </Toggle>
}
```

### Disabled

```jsx
{
  globals: {
    imports: `import { Toggle, ToggleControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle disabled>
      <ToggleControl />
    </Toggle>
}
```

### Invalid

```jsx
{
  globals: {
    imports: `import { Toggle, ToggleControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle invalid>
      <ToggleControl />
    </Toggle>
}
```

### With label

```jsx
{
  globals: {
    imports: `import { Toggle, ToggleControl, ToggleLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle>
      <ToggleControl />
      <ToggleLabel>
        Enable dark mode      </ToggleLabel>
    </Toggle>
}
```

### With inside labels

```jsx
{
  globals: {
    imports: `import { Toggle, ToggleControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle withLabels>
      <ToggleControl />
    </Toggle>
}
```

### Form field

```jsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { TEXT_PRESET, FormField, Text, Toggle, ToggleControl, ToggleLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Text preset={TEXT_PRESET.label}>
        Notification settings:      </Text>
      <FormField>
        <Toggle>
          <ToggleControl />
          <ToggleLabel>
            General Information          </ToggleLabel>
        </Toggle>
      </FormField>
      <FormField>
        <Toggle>
          <ToggleControl />
          <ToggleLabel>
            Promotions          </ToggleLabel>
        </Toggle>
      </FormField>
    </>
}
```

## Recipes

---

No recipe defined for now.

## React Components/Toggle

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `checked` | `` | No |  | The controlled checked state of the toggle. |
| `defaultChecked` | `` | No |  | The initial checked state of the toggle. Use when you don't need to control the checked state of the toggle. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `invalid` | `` | No |  | Whether the component is in error state. |
| `name` | `` | No |  | The name of the form element. Useful for form submission. |
| `onCheckedChange` | `` | No |  | Callback fired when the checked state changes. |
| `required` | `` | No |  | Whether the component is required. |
| `value` | `` | No |  | The value of form element. Useful for form submission. |
| `withLabels` | `` | No |  | Whether the component displays "ON/OFF" labels. |


## Subcomponents


### ToggleControl




### ToggleLabel



## Examples


### Accessibility Aria Label

```tsx
{
  globals: {
    imports: `import { Toggle, ToggleControl, ToggleLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle aria-label="Enable dark mode">
      <ToggleControl />

      <ToggleLabel>
        Dark mode
      </ToggleLabel>
    </Toggle>
}
```

### Accessibility Label

```tsx
{
  globals: {
    imports: `import { Toggle, ToggleControl, ToggleLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle>
      <ToggleControl />

      <ToggleLabel>
        Enable dark mode
      </ToggleLabel>
    </Toggle>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Toggle>
      <ToggleControl />

      <ToggleLabel>
        Toggle label
      </ToggleLabel>
    </Toggle>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Toggle, ToggleControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle>
      <ToggleControl />
    </Toggle>
}
```

### Demo

```tsx
{
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    invalid: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    withLabels: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { Toggle, ToggleControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle disabled>
      <ToggleControl />
    </Toggle>
}
```

### In Form Field

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'column',
    gap: '8px'
  }}>{story()}</div>],
  globals: {
    imports: `import { TEXT_PRESET, FormField, Text, Toggle, ToggleControl, ToggleLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Text preset={TEXT_PRESET.label}>
        Notification settings:
      </Text>

      <FormField>
        <Toggle>
          <ToggleControl />

          <ToggleLabel>
            General Information
          </ToggleLabel>
        </Toggle>
      </FormField>

      <FormField>
        <Toggle>
          <ToggleControl />

          <ToggleLabel>
            Promotions
          </ToggleLabel>
        </Toggle>
      </FormField>
    </>
}
```

### Invalid

```tsx
{
  globals: {
    imports: `import { Toggle, ToggleControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle invalid>
      <ToggleControl />
    </Toggle>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Toggle>
      <ToggleControl />
    </Toggle>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    gap: '24px',
    alignItems: 'center'
  }}>
      <Toggle>
        <ToggleControl />
      </Toggle>

      <Toggle disabled>
        <ToggleControl />
      </Toggle>

      <Toggle invalid>
        <ToggleControl />
      </Toggle>

      <Toggle withLabels>
        <ToggleControl />
      </Toggle>

      <Toggle>
        <ToggleControl />
        <ToggleLabel>With label</ToggleLabel>
      </Toggle>
    </div>
}
```

### With Label

```tsx
{
  globals: {
    imports: `import { Toggle, ToggleControl, ToggleLabel } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle>
      <ToggleControl />

      <ToggleLabel>
        Enable dark mode
      </ToggleLabel>
    </Toggle>
}
```

### With Labels

```tsx
{
  globals: {
    imports: `import { Toggle, ToggleControl } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Toggle withLabels>
      <ToggleControl />
    </Toggle>
}
```

## React Components

# Tooltip

_**Tooltip** component is used to display contextual information when the user hovers or focuses a UI element in a page._

## Overview

---

**Tooltip** component is used to display contextual information when the user hovers or focuses an element.

It enhances user experience by providing helpful hints, explanations, or descriptions without cluttering the UI.

**Tooltips** are commonly used with a button, an icon, or interactive elements.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Tooltip</td></tr><tr><th scope="row">Also known as</th><td>Info Bubble</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/ODS---UI-Kit?node-id=55-23473" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/tooltip" target="_blank">Github <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://ovh.github.io/design-system/v18.6.4/?path=/docs/ods-components-tooltip--documentation" target="_blank">Previous major version<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

Use the **Tooltip** component to provide additional information or context for UI elements.

**Tooltips** should be brief and helpful, enhancing user understanding without overwhelming them.

They are suitable for a button, an icon, a link, and any other elements where additional context would be beneficial.

### Tooltip vs Popover

-   Both components look similar but a **Tooltip** is displayed on hover and focus while a  is triggered by click.
-   **Tooltips** are commonly used for shorter explanations, while longer text / complex UIs would suit Popovers better.
-   Use a Popover when you need to insert interactive elements such as a button.
-   A Popover can be dismissed if an action button allows it.

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Tooltip to provide concise, contextual help or clarification |
| - Keep the content brief and easy to read, ideally under two lines |
| - Use complete sentences with punctuation when space allows for improved readability |
| - Use Tooltips only when the user might need additional context, not by default on every element |
| - Position the Tooltip so it does not obstruct important elements |

| ❌ Don't |
| --- |
| - Don't place critical or essential information inside a Tooltip, users may miss it |
| - Don't use Tooltip to show error messages |
| - Don't include interactive elements (e.g., buttons, links) inside a Tooltip |
| - Don't repeat content already visible in the UI. Tooltips should complement, not duplicate |
| - Don't overload Tooltips with rich content (images, media, long paragraphs) |

### Best Practices in Context

1.  **Tooltip**
2.  **Content**
3.  **Caret tip** - optional
4.  **Trigger**

## Placement

---

**Tooltip** has automatic positioning feature. It can detect the edge of the browser so the container always stays visible on a page.

## Behavior

---

By default, a **Tooltip** is hidden to the user.

It triggers when the user hovers or focuses on the **Tooltip**'s trigger element such as an icon.

The **Tooltip** remains visible while the user's mouse or focus is active, with customizable open and close delays.

It will hide when the users stop hovering over or focusing on this element.

On a mobile device, a **Tooltip** is displayed upon tapping, and it will hide when the user taps an area outside the **Tooltip** container.

## Variation

---

**With extra arrow**: providing a more pronounced visual connection between the tooltip and the element it describes, ensuring users clearly understand which element the tooltip is associated with.

## Navigation

---

### Focus Management

The **Tooltip** is tied to its trigger element and becomes visible when the trigger receives focus.

It remains open as long as the trigger element is focused.

### General Keyboard Shortcuts

Pressing Tab moves focus to the trigger, opening the **Tooltip** without delay.

Pressing Tab again moves focus away, closing the **Tooltip**.

Pressing Escape closes the **Tooltip** immediately if it is open.

## Accessibility

---

**Tooltips** are used to provide supplementary information about a UI element. This component complies with the [Tooltip WAI-ARIA design pattern](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/tooltip_role) .

You should set a minimal **Tooltip Trigger** height to at least `24px` to provide sufficient visual clarity.

### Use tooltips to provide additional context

If your UI includes an unlabeled or ambiguous element, consider attaching a **Tooltip** that explains its purpose.

```jsx
<Tooltip>
  <TooltipTrigger asChild>
    <Icon
      aria-label="Open tooltip"
      name="circle-info"
      role="img"
      style={{
        fontSize: '24px'
      }}
    />
  </TooltipTrigger>
  <TooltipContent>
    Some additional context.  </TooltipContent>
</Tooltip>
```

Screens will announce the tooltip information.

## React Components

# Tooltip

## Overview

---

## Anatomy

---

Tooltip

TooltipContent

TooltipTrigger

---

## Tooltip

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
closeDelay

 | `number` | - | `undefined` | Number of milliseconds before closing the tooltip. |
| 

onOpenChange

 | `(detail: TooltipOpenChangeDetail) => void` | - | `undefined` | Callback fired when the tooltip open state changes. |
| 

open

 | `boolean` | - | `undefined` | The controlled open state of the tooltip. |
| 

openDelay

 | `number` | - | `undefined` | Number of milliseconds before opening the tooltip. |
| 

overlayConfig

 | `object` | - | `undefined` | The overlay configuration. |
| 

flip

 | `boolean` | - | `-` | Whether to flip the position. |
| 

gutter

 | `number` | - | `-` | The main axis offset or gap between the reference and floating elements. |
| 

position

 | `TOOLTIP_POSITION` | - | `-` | The tooltip position around the trigger. |
| 

sameWidth

 | `boolean` | - | `-` | Whether to make the floating element same width as the reference element. |
| 

positionDeprecated

 | `TOOLTIP_POSITION` | - | `undefined` | The tooltip position around the trigger. |
| 

positionerStyle

 | `CSSProperties` | - | `undefined` | Custom style applied to the overlay positioner. Useful if you want to override the overlay z-index. |

## TooltipContent

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/div#attributes) . |
| 
createPortal

 | `boolean` | - | `true` | Whether the component should be rendered in the DOM close to the body tag. |
| 

withArrow

 | `boolean` | - | `false` | Whether the component displays an arrow pointing to the trigger. |

## TooltipTrigger

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| This component extends all the native [button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#attributes) . |
| 
asChild

 | `boolean` | - | `undefined` | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |

## Enums

---

### TOOLTIP_POSITION

-   bottom =`"bottom"`
-   bottomEnd =`"bottom-end"`
-   bottomStart =`"bottom-start"`
-   left =`"left"`
-   leftEnd =`"left-end"`
-   leftStart =`"left-start"`
-   right =`"right"`
-   rightEnd =`"right-end"`
-   rightStart =`"right-start"`
-   top =`"top"`
-   topEnd =`"top-end"`
-   topStart =`"top-start"`

## Interfaces

---

### TooltipOpenChangeDetail

-   `open: boolean`

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tooltip>
      <TooltipTrigger>
        Show tooltip      </TooltipTrigger>
      <TooltipContent>
        This is the tooltip content      </TooltipContent>
    </Tooltip>
}
```

### Custom Trigger

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tooltip>
      <TooltipTrigger asChild>
        <Icon name={ICON_NAME.circleQuestion} style={{
        fontSize: '24px'
      }} />
      </TooltipTrigger>
      <TooltipContent>
        This is the tooltip content      </TooltipContent>
    </Tooltip>
}
```

### Controlled

```jsx
const [isOpen, setIsOpen] = useState(false);
  function toggleTooltip() {
    setIsOpen(isOpen => !isOpen);
  }
  return <>
      <Button onClick={toggleTooltip}>
        Toggle tooltip      </Button>
      <Tooltip open={isOpen}>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleQuestion} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>
        <TooltipContent withArrow>
          This is the tooltip content        </TooltipContent>
      </Tooltip>
    </>;
}
```

### Grid

```jsx
{
  decorators: [story => <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(3, 1fr)',
    gridTemplateRows: 'repeat(3, 1fr)',
    gap: '20px',
    padding: '50px 150px'
  }}>
      {story()}
    </div>],
  globals: {
    imports: `import { Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Tooltip overlayConfig={{
      position: 'top-start'
    }}>
        <TooltipTrigger>
          Top Left        </TooltipTrigger>
        <TooltipContent withArrow>
          Top Left tooltip        </TooltipContent>
      </Tooltip>
      <Tooltip overlayConfig={{
      position: 'top'
    }}>
        <TooltipTrigger>
          Top        </TooltipTrigger>
        <TooltipContent withArrow>
          Top tooltip        </TooltipContent>
      </Tooltip>
      <Tooltip overlayConfig={{
      position: 'top-end'
    }}>
        <TooltipTrigger>
          Top Right        </TooltipTrigger>
        <TooltipContent withArrow>
          Top Right tooltip        </TooltipContent>
      </Tooltip>
      <Tooltip overlayConfig={{
      position: 'left'
    }}>
        <TooltipTrigger>
          Middle Left        </TooltipTrigger>
        <TooltipContent withArrow>
          Middle Left tooltip        </TooltipContent>
      </Tooltip>
      <div />
      <Tooltip overlayConfig={{
      position: 'right'
    }}>
        <TooltipTrigger>
          Middle Right        </TooltipTrigger>
        <TooltipContent withArrow>
          Middle Right tooltip        </TooltipContent>
      </Tooltip>
      <Tooltip overlayConfig={{
      position: 'bottom-start'
    }}>
        <TooltipTrigger>
          Bottom Left        </TooltipTrigger>
        <TooltipContent withArrow>
          Bottom Left tooltip        </TooltipContent>
      </Tooltip>
      <Tooltip overlayConfig={{
      position: 'bottom'
    }}>
        <TooltipTrigger>
          Bottom        </TooltipTrigger>
        <TooltipContent withArrow>
          Bottom tooltip        </TooltipContent>
      </Tooltip>
      <Tooltip overlayConfig={{
      position: 'bottom-end'
    }}>
        <TooltipTrigger>
          Bottom Right        </TooltipTrigger>
        <TooltipContent withArrow>
          Bottom Right tooltip        </TooltipContent>
      </Tooltip>
    </>
}
```

### Z index

```jsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, TOOLTIP_POSITION, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>Default Z-axis order:</span>
      <Tooltip open>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>
        <TooltipContent withArrow>
          Back        </TooltipContent>
      </Tooltip>
      <Tooltip open>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>
        <TooltipContent withArrow>
          Front        </TooltipContent>
      </Tooltip>
      <br />
      <span>Custom Z-axis order:</span>
      <Tooltip open overlayConfig={{
      position: TOOLTIP_POSITION.bottom
    }} positionerStyle={{
      zIndex: 'calc(var(--ods-theme-overlay-z-index) + 1)'
    }}>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>
        <TooltipContent withArrow>
          Front        </TooltipContent>
      </Tooltip>
      <Tooltip open overlayConfig={{
      position: TOOLTIP_POSITION.bottom
    }}>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>
        <TooltipContent withArrow>
          Back        </TooltipContent>
      </Tooltip>
    </>
}
```

## Recipes

---

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Feature List

-   Memory: up to 1.5 TB
    
-   SLA: 99.99%
    
-   Guaranteed public bandwidth from 5 Gbps to 25 Gbps
    
-   25 Gbps private bandwidth included
    
-   OVHcloud Link Aggregation
    

Feature List Product Card

WEB HOSTING

NewBest seller

Performance

For demanding online stores and projects.

1 vCore 2,4 GHz, 2 Go RAM1 vCore 2,4 GHz, 4 Go RAM2 vCores 2,4 GHz, 8 Go RAM

From

€24.46ex. VAT/month

or €13.19 incl. VAT/monthfor a 24-month registration

Minimum 2-year registration €100 free with a 5-year registration

Installation fee:Free

-   -   Unlimited websites
        
    -   High power level
        
    -   1 domain name free for the first
        
    -   500 GB SSD storage
        
    -   1,000 email addresses
        
-   1-click CMS
    
    -   WordPress
        
    -   Joomla!
        
    -   Drupal
        
    -   Prestashop
        
-   Database
    
    -   4 x 1 GB databases
        
    -   8 GB Web Cloud Databases
        
-   Security
    
    -   Unlimited free SSL
        
    -   Anti-DDoS protection
        
    -   Anti-virus and anti-spam
        
    -   Daily backups
        
-   Performance
    
    -   99.9% observed availability
        
    -   Guaranteed resources
        
    -   Unlimited traffic
        
    -   Service continuity
        
    -   Boost option to withstand temporary traffic spikes
        
-   Support and additional services
    
    -   Git
        
    -   Standard support
        
    -   SSH access
        
    -   CDN Basic

## React Components/Tooltip

## Subcomponents


### TooltipContent



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `createPortal` | `` | No | true | Whether the component should be rendered in the DOM close to the body tag. |
| `withArrow` | `` | No | false | Whether the component displays an arrow pointing to the trigger. |



### TooltipTrigger



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `asChild` | `` | No |  | Use the provided child element as the default rendered element, combining their props and behavior. Be careful to pass an actual Node, not a Fragment. |


## Examples


### Accessibility Tooltip

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tooltip>
      <TooltipTrigger asChild>
        <Icon aria-label="Open tooltip" name={ICON_NAME.circleInfo} role="img" style={{
        fontSize: '24px'
      }} />
      </TooltipTrigger>

      <TooltipContent>
        Some additional context.
      </TooltipContent>
    </Tooltip>
}
```

### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => <Tooltip open overlayConfig={{
    flip: false,
    position: TOOLTIP_POSITION.top
  }}>
      <TooltipTrigger asChild>
        <Icon name={ICON_NAME.circleQuestion} style={{
        fontSize: '24px'
      }} />
      </TooltipTrigger>

      <TooltipContent createPortal={false}>
        This is the tooltip content
      </TooltipContent>
    </Tooltip>
}
```

### Controlled

```tsx
{
  decorators: [story => <div style={{
    display: 'flex',
    flexFlow: 'row',
    gap: '8px',
    alignItems: 'center'
  }}>{story()}</div>],
  globals: {
    imports: `import { ICON_NAME, Button, Icon, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  tags: ['!dev'],
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  render: ({}) => {
    const [isOpen, setIsOpen] = useState(false);
    function toggleTooltip() {
      setIsOpen(isOpen => !isOpen);
    }
    return <>
        <Button onClick={toggleTooltip}>
          Toggle tooltip
        </Button>

        <Tooltip open={isOpen}>
          <TooltipTrigger asChild>
            <Icon name={ICON_NAME.circleQuestion} style={{
            fontSize: '24px'
          }} />
          </TooltipTrigger>

          <TooltipContent withArrow>
            This is the tooltip content
          </TooltipContent>
        </Tooltip>
      </>;
  }
}
```

### Custom Trigger

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tooltip>
      <TooltipTrigger asChild>
        <Icon name={ICON_NAME.circleQuestion} style={{
        fontSize: '24px'
      }} />
      </TooltipTrigger>

      <TooltipContent>
        This is the tooltip content
      </TooltipContent>
    </Tooltip>
}
```

### Default

```tsx
{
  globals: {
    imports: `import { Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <Tooltip>
      <TooltipTrigger>
        Show tooltip
      </TooltipTrigger>

      <TooltipContent>
        This is the tooltip content
      </TooltipContent>
    </Tooltip>
}
```

### Demo

```tsx
{
  parameters: {
    layout: 'centered'
  },
  render: (arg: DemoArg) => <Tooltip closeDelay={arg.closeDelay} openDelay={arg.openDelay} overlayConfig={{
    gutter: arg.gutter,
    position: arg.position,
    sameWidth: arg.sameWidth
  }}>
      <TooltipTrigger asChild>
        <Icon name={ICON_NAME.circleQuestion} style={{
        fontSize: '24px'
      }} />
      </TooltipTrigger>

      <TooltipContent withArrow={arg.withArrow}>
        {arg.content}
      </TooltipContent>
    </Tooltip>,
  argTypes: orderControls({
    closeDelay: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'number'
    },
    content: {
      table: {
        category: CONTROL_CATEGORY.slot
      },
      control: 'text'
    },
    gutter: {
      table: {
        category: CONTROL_CATEGORY.design,
        type: {
          summary: 'number'
        }
      },
      control: 'number'
    },
    openDelay: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'number'
    },
    position: {
      table: {
        category: CONTROL_CATEGORY.general,
        type: {
          summary: 'TOOLTIP_POSITION'
        }
      },
      control: {
        type: 'select'
      },
      options: TOOLTIP_POSITIONS
    },
    sameWidth: {
      table: {
        category: CONTROL_CATEGORY.design
      },
      control: {
        type: 'boolean'
      }
    },
    withArrow: {
      table: {
        category: CONTROL_CATEGORY.design,
        defaultValue: {
          summary: false
        },
        type: {
          summary: 'boolean'
        }
      },
      control: {
        type: 'boolean'
      }
    }
  }),
  args: {
    content: 'My tooltip content'
  }
}
```

### Grid

```tsx
{
  decorators: [story => <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(3, 1fr)',
    gridTemplateRows: 'repeat(3, 1fr)',
    gap: '20px',
    padding: '50px 150px'
  }}>
      {story()}
    </div>],
  globals: {
    imports: `import { Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <Tooltip overlayConfig={{
      position: 'top-start'
    }}>
        <TooltipTrigger>
          Top Left
        </TooltipTrigger>
        <TooltipContent withArrow>
          Top Left tooltip
        </TooltipContent>
      </Tooltip>

      <Tooltip overlayConfig={{
      position: 'top'
    }}>
        <TooltipTrigger>
          Top
        </TooltipTrigger>
        <TooltipContent withArrow>
          Top tooltip
        </TooltipContent>
      </Tooltip>

      <Tooltip overlayConfig={{
      position: 'top-end'
    }}>
        <TooltipTrigger>
          Top Right
        </TooltipTrigger>
        <TooltipContent withArrow>
          Top Right tooltip
        </TooltipContent>
      </Tooltip>

      <Tooltip overlayConfig={{
      position: 'left'
    }}>
        <TooltipTrigger>
          Middle Left
        </TooltipTrigger>
        <TooltipContent withArrow>
          Middle Left tooltip
        </TooltipContent>
      </Tooltip>

      <div />

      <Tooltip overlayConfig={{
      position: 'right'
    }}>
        <TooltipTrigger>
          Middle Right
        </TooltipTrigger>
        <TooltipContent withArrow>
          Middle Right tooltip
        </TooltipContent>
      </Tooltip>

      <Tooltip overlayConfig={{
      position: 'bottom-start'
    }}>
        <TooltipTrigger>
          Bottom Left
        </TooltipTrigger>
        <TooltipContent withArrow>
          Bottom Left tooltip
        </TooltipContent>
      </Tooltip>

      <Tooltip overlayConfig={{
      position: 'bottom'
    }}>
        <TooltipTrigger>
          Bottom
        </TooltipTrigger>
        <TooltipContent withArrow>
          Bottom tooltip
        </TooltipContent>
      </Tooltip>

      <Tooltip overlayConfig={{
      position: 'bottom-end'
    }}>
        <TooltipTrigger>
          Bottom Right
        </TooltipTrigger>
        <TooltipContent withArrow>
          Bottom Right tooltip
        </TooltipContent>
      </Tooltip>
    </>
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  parameters: {
    layout: 'centered'
  },
  render: ({}) => <Tooltip>
      <TooltipTrigger asChild>
        <Icon name={ICON_NAME.circleQuestion} style={{
        fontSize: '24px'
      }} />
      </TooltipTrigger>

      <TooltipContent>
        This is the tooltip content
      </TooltipContent>
    </Tooltip>
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => <div style={{
    display: 'flex',
    gap: '16px',
    alignItems: 'center'
  }}>
      <Tooltip>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleQuestion} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>
        <TooltipContent createPortal={false}>This is the tooltip content</TooltipContent>
      </Tooltip>

      <Tooltip>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>
        <TooltipContent createPortal={false} withArrow>This is the tooltip content</TooltipContent>
      </Tooltip>
    </div>
}
```

### Z Index

```tsx
{
  globals: {
    imports: `import { ICON_NAME, Icon, TOOLTIP_POSITION, Tooltip, TooltipContent, TooltipTrigger } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => <>
      <span>Default Z-axis order:</span>

      <Tooltip open>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>

        <TooltipContent withArrow>
          Back
        </TooltipContent>
      </Tooltip>

      <Tooltip open>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>

        <TooltipContent withArrow>
          Front
        </TooltipContent>
      </Tooltip>

      <br />

      <span>Custom Z-axis order:</span>

      <Tooltip open overlayConfig={{
      position: TOOLTIP_POSITION.bottom
    }} positionerStyle={{
      zIndex: 'calc(var(--ods-theme-overlay-z-index) + 1)'
    }}>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>

        <TooltipContent withArrow>
          Front
        </TooltipContent>
      </Tooltip>

      <Tooltip open overlayConfig={{
      position: TOOLTIP_POSITION.bottom
    }}>
        <TooltipTrigger asChild>
          <Icon name={ICON_NAME.circleInfo} style={{
          fontSize: '24px'
        }} />
        </TooltipTrigger>

        <TooltipContent withArrow>
          Back
        </TooltipContent>
      </Tooltip>
    </>
}
```

## React Components

# Tree View

src

app.tsx

index.ts

components

Button.tsx

Card.tsx

package.json

README.md

## Overview

---

The **Tree View** component displays hierarchical data in a parent-child structure. It allows users to expand, collapse, and interact with nested items.

<table class="_table_e3iru_2 _table--md_e3iru_59 _table--striped_e3iru_167 _identity-card__table_75e8e_7" data-ods="table" data-storybook="table"><tbody><tr><th scope="row">Name</th><td>Tree View</td></tr><tr><th scope="row">Also known as</th><td>-</td></tr><tr><th scope="row">Links</th><td><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://www.figma.com/design/9jDDTcR4a9jPRFcdjawAlf/branch/8NZfqeptysMQInF02WaIJR" target="_blank">Design <span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a><a class="_link_1qra4_2 _identity-card__app-link_75e8e_12" data-ods="link" href="https://github.com/ovh/design-system/tree/master/packages/ods-react/src/components/tree-view" target="_blank">Github<span class="_icon_g76et_2 _icon--external-link_g76et_256" data-ods="icon" role="presentation"></span></a></td></tr></tbody></table>

## Usage

---

The **Tree View** component is used to navigate or manage hierarchical datasets, for representing file systems, nested categories, any structured dataset that benefits from a collapsible interface.

Common use cases include:

-   Directory structures
-   Nested navigation menus
-   Category selectors
-   Configurations and settings with nested options?

### Dos & Don'ts

| ✅ Do |
| --- |
| - Use a Tree View for hierarchical data where relationships between items are important |

| ❌ Don't |
| --- |
| - Don’t use a Tree View if there are no nodes or if the content is too minimal to justify a hierarchical structure |

### Best Practices in Context

1.  **Tree View**
2.  **Node/item**
3.  **Checkbox (multi-selection)**
4.  **Node/item name**

## Behavior

---

### Expand / Collapse

-   Items/Nodes can be expanded or collapsed to show or hide their children
-   The Tree View can be rendered with all nodes expanded by default
-   Individual nodes can be configured to start in an expanded state when the Tree View is rendered

### Selection

#### Single Selection

-   Clicking a node selects it and deselects any previously selected node

#### Checkbox tree/multi-selection

-   Each node/item has a checkbox for selection
-   Parent checkbox can represent:
    -   Checked: all children are selected
    -   Unchecked: no children are selected
    -   Indeterminate: partial selection of children
-   Selecting a parent node/item selects or deselects all children recursively

### Disabling

Disabled nodes cannot be selected, expanded, collapsed, or interact with. They are still display in the hierarchy for context.

## Navigation

---

### Focus Management

Only one node in the Tree View is focusable at a time.

Focus remains on a single visible node and moves predictably with keyboard navigation.

If a node contains interactive elements (e.g., a checkbox or link), those elements can receive focus.

### General Keyboard Shortcuts

Pressing Arrow Up / Arrow Down moves focus to the previous or next visible node.

Pressing Arrow Right:

-   If the focused node is collapsed, expands it
-   If the focused node is expanded, moves focus to its first child

Pressing Arrow Left:

-   If the focused node is expanded, collapses it
-   If the focused node is collapsed, moves focus to its parent

Pressing Home, or fn + Arrow Left, moves focus to the first visible node.

Pressing End, or fn + Arrow Right moves focus to the last visible node.

Pressing Enter or Space activates, expands, or collapses the focused node. If the node has a checkbox, it toggles its state.

Pressing Enter or Space + Shift + Arrow Up / Arrow Down selects a continuous range of nodes (multi-selection mode).

Pressing Enter or Space + Ctrl or Cmd toggles selection of individual node.

Pressing Ctrl or Cmd + A selects all nodes in the tree. When all nodes are already selected, it unselects all nodes.

Typing an alphanumeric character (`A-Z`, `a-z`) changes focus to the next node starting with that character.

## Accessibility

---

This component complies with the [Tree View WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/)

## React Components

# Tree View

## Overview

---

## Anatomy

---

TreeView

TreeViewNode

TreeViewNodes

---

## TreeView

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
defaultExpandedValue

 | `string[]` | - | `undefined` | The initial expanded value(s). Use when you don't need to control the expanded value(s) of the tree view. |
| 

defaultValue

 | `string[]` | - | `undefined` | The initial selected value(s). Use when you don't need to control the selected value(s) of the tree view. |
| 

disabled

 | `boolean` | - | `undefined` | Whether the component is disabled. |
| 

expandedValue

 | `string[]` | - | `undefined` | The controlled expanded value(s). |
| 

items

 | `Array<TreeViewItem>` |  | `undefined` | The list of items |
| 

multiple

 | `boolean` | - | `undefined` | Whether the multiple selection is allowed. |
| 

onExpandedChange

 | `(details: TreeViewExpandedChangeDetail) => void` | - | `undefined` | Callback fired when the expanded value(s) changes. |
| 

onValueChange

 | `(details: TreeViewValueChangeDetail) => void` | - | `undefined` | Callback fired when the value(s) changes. |
| 

value

 | `string[]` | - | `undefined` | The controlled selected value(s). |

## TreeViewNode

---

| Property | Type | Required | Default value | Description |
| --- | --- | --- | --- | --- |
| 
children

 | `ReactNode | ((arg: TreeViewCustomRendererArg) => ReactNode)` | - | `undefined` | Label content or custom render function. |
| 

item

 | `TreeViewItem` |  | `undefined` | The tree node to render. |

## TreeViewNodes

---

This component has no specific properties.

## Interfaces

---

### TreeViewCustomRendererArg<T>

-   `customData?: T`
-   `isBranch: boolean`
-   `isExpanded: boolean`
-   `item: TreeViewItem<T>`

### TreeViewExpandedChangeDetail

-   `expandedValue: string[]`

### TreeViewItem<T>

-   `children?: TreeViewItem[]`
-   `customRendererData?: T`
-   `disabled?: boolean`
-   `expanded?: boolean`
-   `id: string`
-   `name: string`

### TreeViewValueChangeDetail

-   `value: string[]`

## Css Variables

---

| Token | Value | Preview |
| --- | --- | --- |
| --ods-tree-view-node-border-radius | calc(var(--ods-theme-border-radius) / 2) | 
 |
| --ods-tree-view-node-column-gap | var(--ods-theme-column-gap) | 

 |
| --ods-tree-view-node-padding-horizontal | var(--ods-theme-padding-horizontal) | 

 |
| --ods-tree-view-node-padding-vertical | calc(var(--ods-theme-padding-horizontal) / 2) | 

 |
| --ods-tree-view-node-row-gap | var(--ods-theme-row-gap) | 

 |

## Examples

---

### Default

```jsx
{
  globals: {
    imports: `import { TreeView, TreeViewNode, TreeViewNodes } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView items={items}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>;
  }
}
```

### Multiple

```jsx
const items = [{
    id: 'src',
    name: 'src',
    children: [{
      id: 'app.tsx',
      name: 'app.tsx'
    }, {
      id: 'index.ts',
      name: 'index.ts'
    }, {
      id: 'components',
      name: 'components',
      children: [{
        id: 'Button.tsx',
        name: 'Button.tsx'
      }, {
        id: 'Card.tsx',
        name: 'Card.tsx'
      }]
    }]
  }, {
    id: 'package.json',
    name: 'package.json'
  }, {
    id: 'readme.md',
    name: 'README.md'
  }];
  return <TreeView items={items} multiple>
      <TreeViewNodes>
        {items.map(item => <TreeViewNode key={item.id} item={item} />)}
      </TreeViewNodes>
    </TreeView>;
}
```

### Disabled

```jsx
const items = [{
    id: 'src',
    name: 'src',
    children: [{
      id: 'app.tsx',
      name: 'app.tsx'
    }, {
      id: 'index.ts',
      name: 'index.ts'
    }, {
      id: 'components',
      name: 'components',
      children: [{
        id: 'Button.tsx',
        name: 'Button.tsx'
      }, {
        id: 'Card.tsx',
        name: 'Card.tsx'
      }]
    }]
  }, {
    id: 'package.json',
    name: 'package.json'
  }, {
    id: 'readme.md',
    name: 'README.md'
  }];
  return <TreeView disabled items={items}>
      <TreeViewNodes>
        {items.map(item => <TreeViewNode key={item.id} item={item} />)}
      </TreeViewNodes>
    </TreeView>;
}
```

### Custom render

src

app.tsx

index.ts

components

Button.tsx

Card.tsx

package.json

README.md

```jsx
const items = [{
    id: 'src',
    name: 'src',
    children: [{
      id: 'app.tsx',
      name: 'app.tsx'
    }, {
      id: 'index.ts',
      name: 'index.ts'
    }, {
      id: 'components',
      name: 'components',
      children: [{
        id: 'Button.tsx',
        name: 'Button.tsx'
      }, {
        id: 'Card.tsx',
        name: 'Card.tsx'
      }]
    }]
  }, {
    id: 'package.json',
    name: 'package.json'
  }, {
    id: 'readme.md',
    name: 'README.md'
  }];
  return <TreeView items={items}>
      <TreeViewNodes>
        {items.map(item => <TreeViewNode key={item.id} item={item}>
            {({
          item,
          isBranch,
          isExpanded        }) => <span style={{
          display: 'inline-flex',
          alignItems: 'center',
          gap: 6
        }}>
                {isBranch ? isExpanded ? <Icon name={ICON_NAME.folderMinus} /> : <Icon name={ICON_NAME.folderPlus} /> : <Icon name={ICON_NAME.file} />}
                <span>{item.name}</span>
              </span>}
          </TreeViewNode>)}
      </TreeViewNodes>
    </TreeView>;
}
```

### Default expanded

```jsx
const items = [{
    id: 'src',
    name: 'src',
    children: [{
      id: 'app.tsx',
      name: 'app.tsx'
    }, {
      id: 'index.ts',
      name: 'index.ts'
    }, {
      id: 'components',
      name: 'components',
      children: [{
        id: 'Button.tsx',
        name: 'Button.tsx'
      }, {
        id: 'Card.tsx',
        name: 'Card.tsx'
      }]
    }]
  }, {
    id: 'package.json',
    name: 'package.json'
  }, {
    id: 'readme.md',
    name: 'README.md'
  }];
  return <TreeView defaultExpandedValue={["src", "components"]} items={items}>
      <TreeViewNodes>
        {items.map(item => <TreeViewNode key={item.id} item={item} />)}
      </TreeViewNodes>
    </TreeView>;
}
```

### Controlled

```jsx
const items = [{
    id: 'src',
    name: 'src',
    children: [{
      id: 'app.tsx',
      name: 'app.tsx'
    }, {
      id: 'index.ts',
      name: 'index.ts'
    }, {
      id: 'components',
      name: 'components',
      children: [{
        id: 'Button.tsx',
        name: 'Button.tsx'
      }, {
        id: 'Card.tsx',
        name: 'Card.tsx'
      }]
    }]
  }, {
    id: 'package.json',
    name: 'package.json'
  }, {
    id: 'readme.md',
    name: 'README.md'
  }];
  const [selectedId, setSelectedId] = useState<string | undefined>('package.json');
  return <>
      <TreeView items={items} onValueChange={(d: TreeViewValueChangeDetail) => setSelectedId(d.value[0])} value={selectedId ? [selectedId] : undefined}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>
      <div style={{
      marginTop: 8
    }}>Selected: {selectedId ?? 'None'}</div>
    </>;
}
```

### Controlled multiple

```jsx
const items = [{
    id: 'src',
    name: 'src',
    children: [{
      id: 'app.tsx',
      name: 'app.tsx'
    }, {
      id: 'index.ts',
      name: 'index.ts'
    }, {
      id: 'components',
      name: 'components',
      children: [{
        id: 'Button.tsx',
        name: 'Button.tsx'
      }, {
        id: 'Card.tsx',
        name: 'Card.tsx'
      }]
    }]
  }, {
    id: 'package.json',
    name: 'package.json'
  }, {
    id: 'readme.md',
    name: 'README.md'
  }];
  const [selectedIds, setSelectedIds] = useState<string[]>(['package.json', 'index.ts']);
  return <>
      <TreeView items={items} multiple onValueChange={(d: TreeViewValueChangeDetail) => setSelectedIds(Array.isArray(d.value) ? d.value : [d.value].filter(Boolean) as string[])} value={selectedIds}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>
      <div style={{
      marginTop: 8
    }}>Selected: {selectedIds.length ? selectedIds.join(', ') : 'None'}</div>
    </>;
}
```

### Disabled items

```jsx
const items = [{
    id: 'src',
    name: 'src',
    children: [{
      id: 'app.tsx',
      name: 'app.tsx'
    }, {
      id: 'index.ts',
      name: 'index.ts',
      disabled: true
    }, {
      id: 'components',
      name: 'components',
      disabled: true,
      children: [{
        id: 'Button.tsx',
        name: 'Button.tsx'
      }, {
        id: 'Card.tsx',
        name: 'Card.tsx',
        disabled: true
      }]
    }]
  }, {
    id: 'package.json',
    name: 'package.json',
    disabled: true
  }, {
    id: 'readme.md',
    name: 'README.md'
  }];
  return <TreeView items={items}>
      <TreeViewNodes>
        {items.map(item => <TreeViewNode key={item.id} item={item} />)}
      </TreeViewNodes>
    </TreeView>;
}
```

### In form field

```jsx
const items = [{
    id: 'src',
    name: 'src',
    children: [{
      id: 'app.tsx',
      name: 'app.tsx'
    }, {
      id: 'index.ts',
      name: 'index.ts'
    }, {
      id: 'components',
      name: 'components',
      children: [{
        id: 'Button.tsx',
        name: 'Button.tsx'
      }, {
        id: 'Card.tsx',
        name: 'Card.tsx'
      }]
    }]
  }, {
    id: 'package.json',
    name: 'package.json'
  }, {
    id: 'readme.md',
    name: 'README.md'
  }];
  return <FormField>
      <FormFieldLabel>Choose a file</FormFieldLabel>
      <TreeView items={items}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>
    </FormField>;
}
```

### Dynamic children

```jsx
type Item = {
    id: string;
    name: string;
    children?: Item[];
  };
  const [items, setItems] = useState<Item[]>([{
    id: 'src',
    name: 'src',
    children: [{
      id: 'app.tsx',
      name: 'app.tsx'
    }, {
      id: 'index.ts',
      name: 'index.ts'
    }, {
      id: 'components',
      name: 'components',
      children: []
    }]
  }, {
    id: 'package.json',
    name: 'package.json'
  }, {
    id: 'readme.md',
    name: 'README.md'
  }]);
  const counter = useRef(1);
  function addChildTo(collection: Item[], parentId: string, newNode: Item): Item[] {
    return collection.map(node => {
      if (node.id === parentId) {
        const nextChildren = Array.isArray(node.children) ? [...node.children, newNode] : [newNode];
        return {
          ...node,
          children: nextChildren        };
      }
      if (node.children?.length) {
        return {
          ...node,
          children: addChildTo(node.children, parentId, newNode)
        };
      }
      return node;
    });
  }
  function removeNodeFrom(collection: Item[], nodeId: string): Item[] {
    return collection.filter(node => node.id !== nodeId).map(node => node.children?.length ? {
      ...node,
      children: removeNodeFrom(node.children, nodeId)
    } : node);
  }
  function handleAddChild(parentId: string): void {
    const id = `new-file-${counter.current++}.txt`;
    const newNode = {
      id,
      name: id    };
    setItems(prev => addChildTo(prev, parentId, newNode));
  }
  function handleDelete(nodeId: string): void {
    setItems(prev => removeNodeFrom(prev, nodeId));
  }
  function handleAddRootFile(): void {
    const id = `new-file-${counter.current++}.txt`;
    const newNode = {
      id,
      name: id    };
    setItems(prev => [...prev, newNode]);
  }
  return <div>
      <div style={{
      marginBottom: 16
    }}>
        <Button aria-label="Add file at root level" onClick={handleAddRootFile} size={BUTTON_SIZE.xs} variant={BUTTON_VARIANT.outline}>
          <Icon name={ICON_NAME.plus} />
          Add file at root level        </Button>
      </div>
      <TreeView items={items} multiple>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item}>
              {({
            item,
            isBranch          }) => <div style={{
            display: 'flex',
            alignItems: 'center',
            width: '100%',
            justifyContent: 'space-between'
          }}>
                  <span style={{
              display: 'inline-flex',
              alignItems: 'center',
              gap: 6
            }}>
                    {isBranch ? <Icon name={ICON_NAME.folder} /> : <Icon name={ICON_NAME.file} />}
                    <span>{item.name}</span>
                  </span>
                  <div style={{
              display: 'inline-flex',
              marginLeft: 'auto',
              alignItems: 'center',
              gap: 8
            }}>
                    {isBranch ? <Button aria-label="Add child" onClick={e => {
                e.stopPropagation();
                handleAddChild(item.id);
              }} size={BUTTON_SIZE.xs} onKeyDown={e => {
                e.stopPropagation();
              }} variant={BUTTON_VARIANT.outline}>
                        <Icon name={ICON_NAME.plus} />
                      </Button> : null}
                    <Button aria-label="Delete" color={BUTTON_COLOR.critical} onClick={e => {
                e.stopPropagation();
                handleDelete(item.id);
              }} onMouseDown={e => {
                e.stopPropagation();
              }} onKeyDown={e => {
                e.stopPropagation();
              }} size={BUTTON_SIZE.xs} variant={BUTTON_VARIANT.outline}>
                      <Icon name={ICON_NAME.trash} />
                    </Button>
                  </div>
                </div>}
            </TreeViewNode>)}
        </TreeViewNodes>
      </TreeView>
    </div>;
}
```

## Recipes

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

## React Components/Tree View

## Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `defaultExpandedValue` | `` | No |  | The initial expanded value(s). Use when you don't need to control the expanded value(s) of the tree view. |
| `defaultValue` | `` | No |  | The initial selected value(s). Use when you don't need to control the selected value(s) of the tree view. |
| `disabled` | `` | No |  | Whether the component is disabled. |
| `expandedValue` | `` | No |  | The controlled expanded value(s). |
| `items` | `` | Yes |  | The list of items |
| `multiple` | `` | No |  | Whether the multiple selection is allowed. |
| `onExpandedChange` | `` | No |  | Callback fired when the expanded value(s) changes. |
| `onValueChange` | `` | No |  | Callback fired when the value(s) changes. |
| `value` | `` | No |  | The controlled selected value(s). |


## Subcomponents


### TreeViewNode



#### Props


| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `indexPath` | `` | No |  | @internal * |
| `item` | `` | Yes |  | The tree node to render. |



### TreeViewNodes



## Examples


### Anatomy Tech

```tsx
{
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView items={items}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>;
  }
}
```

### Controlled

```tsx
{
  globals: {
    imports: `import { TreeView, TreeViewNode, TreeViewNodes, type TreeViewValueChangeDetail } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    const [selectedId, setSelectedId] = useState<string | undefined>('package.json');
    return <>
        <TreeView items={items} onValueChange={(d: TreeViewValueChangeDetail) => setSelectedId(d.value[0])} value={selectedId ? [selectedId] : undefined}>
          <TreeViewNodes>
            {items.map(item => <TreeViewNode key={item.id} item={item} />)}
          </TreeViewNodes>
        </TreeView>
        <div style={{
        marginTop: 8
      }}>Selected: {selectedId ?? 'None'}</div>
      </>;
  }
}
```

### Controlled Multiple

```tsx
{
  globals: {
    imports: `import { TreeView, TreeViewNode, TreeViewNodes, type TreeViewValueChangeDetail } from '@ovhcloud/ods-react';
import { useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    const [selectedIds, setSelectedIds] = useState<string[]>(['package.json', 'index.ts']);
    return <>
        <TreeView items={items} multiple onValueChange={(d: TreeViewValueChangeDetail) => setSelectedIds(Array.isArray(d.value) ? d.value : [d.value].filter(Boolean) as string[])} value={selectedIds}>
          <TreeViewNodes>
            {items.map(item => <TreeViewNode key={item.id} item={item} />)}
          </TreeViewNodes>
        </TreeView>
        <div style={{
        marginTop: 8
      }}>Selected: {selectedIds.length ? selectedIds.join(', ') : 'None'}</div>
      </>;
  }
}
```

### Custom Render

```tsx
{
  globals: {
    imports: `import { Icon, ICON_NAME, TreeView, TreeViewNode, TreeViewNodes } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView items={items}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item}>
              {({
            item,
            isBranch,
            isExpanded
          }) => <span style={{
            display: 'inline-flex',
            alignItems: 'center',
            gap: 6
          }}>
                  {isBranch ? isExpanded ? <Icon name={ICON_NAME.folderMinus} /> : <Icon name={ICON_NAME.folderPlus} /> : <Icon name={ICON_NAME.file} />}
                  <span>{item.name}</span>
                </span>}
            </TreeViewNode>)}
        </TreeViewNodes>
      </TreeView>;
  }
}
```

### Default

```tsx
{
  globals: {
    imports: `import { TreeView, TreeViewNode, TreeViewNodes } from '@ovhcloud/ods-react';`
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView items={items}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>;
  }
}
```

### Default Expanded Value

```tsx
{
  globals: {
    imports: `import { TreeView, TreeViewNode, TreeViewNodes } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView defaultExpandedValue={["src", "components"]} items={items}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>;
  }
}
```

### Demo

```tsx
{
  render: arg => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView defaultExpandedValue={arg.defaultExpandedValue} disabled={arg.disabled} expandedValue={arg.expandedValue} items={items} multiple={arg.multiple}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>;
  },
  argTypes: orderControls({
    disabled: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    },
    multiple: {
      table: {
        category: CONTROL_CATEGORY.general
      },
      control: 'boolean'
    }
  })
}
```

### Disabled

```tsx
{
  globals: {
    imports: `import { TreeView, TreeViewNode, TreeViewNodes } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView disabled items={items}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>;
  }
}
```

### Disabled Items

```tsx
{
  globals: {
    imports: `import { TreeView, TreeViewNode, TreeViewNodes } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts',
        disabled: true
      }, {
        id: 'components',
        name: 'components',
        disabled: true,
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx',
          disabled: true
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json',
      disabled: true
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView items={items}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>;
  }
}
```

### Dynamic Children

```tsx
{
  globals: {
    imports: `import { Button, BUTTON_COLOR, BUTTON_SIZE, BUTTON_VARIANT, Icon, ICON_NAME, TreeView, TreeViewNode, TreeViewNodes } from '@ovhcloud/ods-react';
import { useRef, useState } from 'react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    type Item = {
      id: string;
      name: string;
      children?: Item[];
    };
    const [items, setItems] = useState<Item[]>([{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: []
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }]);
    const counter = useRef(1);
    function addChildTo(collection: Item[], parentId: string, newNode: Item): Item[] {
      return collection.map(node => {
        if (node.id === parentId) {
          const nextChildren = Array.isArray(node.children) ? [...node.children, newNode] : [newNode];
          return {
            ...node,
            children: nextChildren
          };
        }
        if (node.children?.length) {
          return {
            ...node,
            children: addChildTo(node.children, parentId, newNode)
          };
        }
        return node;
      });
    }
    function removeNodeFrom(collection: Item[], nodeId: string): Item[] {
      return collection.filter(node => node.id !== nodeId).map(node => node.children?.length ? {
        ...node,
        children: removeNodeFrom(node.children, nodeId)
      } : node);
    }
    function handleAddChild(parentId: string): void {
      const id = `new-file-${counter.current++}.txt`;
      const newNode = {
        id,
        name: id
      };
      setItems(prev => addChildTo(prev, parentId, newNode));
    }
    function handleDelete(nodeId: string): void {
      setItems(prev => removeNodeFrom(prev, nodeId));
    }
    function handleAddRootFile(): void {
      const id = `new-file-${counter.current++}.txt`;
      const newNode = {
        id,
        name: id
      };
      setItems(prev => [...prev, newNode]);
    }
    return <div>
        <div style={{
        marginBottom: 16
      }}>
          <Button aria-label="Add file at root level" onClick={handleAddRootFile} size={BUTTON_SIZE.xs} variant={BUTTON_VARIANT.outline}>
            <Icon name={ICON_NAME.plus} />
            Add file at root level
          </Button>
        </div>
        <TreeView items={items} multiple>
          <TreeViewNodes>
            {items.map(item => <TreeViewNode key={item.id} item={item}>
                {({
              item,
              isBranch
            }) => <div style={{
              display: 'flex',
              alignItems: 'center',
              width: '100%',
              justifyContent: 'space-between'
            }}>
                    <span style={{
                display: 'inline-flex',
                alignItems: 'center',
                gap: 6
              }}>
                      {isBranch ? <Icon name={ICON_NAME.folder} /> : <Icon name={ICON_NAME.file} />}
                      <span>{item.name}</span>
                    </span>
                    <div style={{
                display: 'inline-flex',
                marginLeft: 'auto',
                alignItems: 'center',
                gap: 8
              }}>
                      {isBranch ? <Button aria-label="Add child" onClick={e => {
                  e.stopPropagation();
                  handleAddChild(item.id);
                }} size={BUTTON_SIZE.xs} onKeyDown={e => {
                  e.stopPropagation();
                }} variant={BUTTON_VARIANT.outline}>
                          <Icon name={ICON_NAME.plus} />
                        </Button> : null}
                      <Button aria-label="Delete" color={BUTTON_COLOR.critical} onClick={e => {
                  e.stopPropagation();
                  handleDelete(item.id);
                }} onMouseDown={e => {
                  e.stopPropagation();
                }} onKeyDown={e => {
                  e.stopPropagation();
                }} size={BUTTON_SIZE.xs} variant={BUTTON_VARIANT.outline}>
                        <Icon name={ICON_NAME.trash} />
                      </Button>
                    </div>
                  </div>}
              </TreeViewNode>)}
          </TreeViewNodes>
        </TreeView>
      </div>;
  }
}
```

### In Form Field

```tsx
{
  globals: {
    imports: `import { FormField, FormFieldLabel, TreeView, TreeViewNode, TreeViewNodes } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <FormField>
        <FormFieldLabel>Choose a file</FormFieldLabel>
        <TreeView items={items}>
          <TreeViewNodes>
            {items.map(item => <TreeViewNode key={item.id} item={item} />)}
          </TreeViewNodes>
        </TreeView>
      </FormField>;
  }
}
```

### Multiple

```tsx
{
  globals: {
    imports: `import { TreeView, TreeViewNode, TreeViewNodes } from '@ovhcloud/ods-react';`
  },
  parameters: {
    docs: {
      source: {
        ...staticSourceRenderConfig()
      }
    }
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView items={items} multiple>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>;
  }
}
```

### Overview

```tsx
{
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'app.tsx',
        name: 'app.tsx'
      }, {
        id: 'index.ts',
        name: 'index.ts'
      }, {
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'Card.tsx',
          name: 'Card.tsx'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }, {
      id: 'readme.md',
      name: 'README.md'
    }];
    return <TreeView items={items}>
        <TreeViewNodes>
          {items.map(item => <TreeViewNode key={item.id} item={item} />)}
        </TreeViewNodes>
      </TreeView>;
  }
}
```

### Theme Generator

```tsx
{
  parameters: {
    layout: 'fullscreen'
  },
  tags: ['!dev'],
  render: ({}) => {
    const items = [{
      id: 'src',
      name: 'src',
      children: [{
        id: 'components',
        name: 'components',
        children: [{
          id: 'Button.tsx',
          name: 'Button.tsx'
        }, {
          id: 'button.scss',
          name: 'button.scss'
        }]
      }]
    }, {
      id: 'package.json',
      name: 'package.json'
    }];
    return <div style={{
      display: 'flex',
      gap: '24px'
    }}>
        <TreeView items={items}>
          <TreeViewNodes>
            {items.map(item => <TreeViewNode key={item.id} item={item} />)}
          </TreeViewNodes>
        </TreeView>

        <TreeView disabled items={items}>
          <TreeViewNodes>
            {items.map(item => <TreeViewNode key={item.id} item={item} />)}
          </TreeViewNodes>
        </TreeView>
      </div>;
  }
}
```

## Recipes

# Components

_Recipes are ready-to-use UI patterns built with ODS components. They provide pre-implemented code snippets for common use cases, helping you build consistent interfaces faster. Each recipe may include multiple implementations, so you can choose the styling approach that fits your project._

Filter recipes:

---

Chat

Assistant2:58 PM

Welcome to the Chat recipe. Feel free to test the UI behavior by typing anything on your mind below.

Config Tile

VPS 1

4 vCore8 Go RAM100 Go 1 day automated backup Unlimited traffic 200 Mbps

12 months6 monthsNo commitment

From€24.46ex. VAT/monthor €13.19 incl. VAT/month

Dashboard Card

#### Cluster Information

---

Name

MyCluster

---

ID

---

Region

GRA91-AZ

---

Admission plugins

Always Pull Images PluginEnable

Plugin Node RestrictionEnable

---

Data Grid

| 
 | 

First Name

 | 

Last Name

 | 

Age

 | 

IP Address

 | 

Actions

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

102550100300

of 0 results

Data Grid With Query Filter

| 
Instance ID

 | 

Location

 | 

Model

 | 

Image

 | 

Backup Logic

 | 

Running since

 |
| --- | --- | --- | --- | --- | --- |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |
| 

 | 

 | 

 | 

 | 

 | 

 |

Email Field

Email- mandatory

@

.fr.com.dev

The part before the email address (the text before the @) must follow these guidelines:

-   It must end with a letter or a number
-   Allowed special characters are: ".", "_", "-"
-   Special characters cannot be placed next to each other

Feature List

-   Memory: up to 1.5 TB
    
-   SLA: 99.99%
    
-   Guaranteed public bandwidth from 5 Gbps to 25 Gbps
    
-   25 Gbps private bandwidth included
    
-   OVHcloud Link Aggregation
    

Feature List Product Card

WEB HOSTING

NewBest seller

Performance

For demanding online stores and projects.

1 vCore 2,4 GHz, 2 Go RAM1 vCore 2,4 GHz, 4 Go RAM2 vCores 2,4 GHz, 8 Go RAM

From

€24.46ex. VAT/month

or €13.19 incl. VAT/monthfor a 24-month registration

Minimum 2-year registration €100 free with a 5-year registration

Installation fee:Free

-   -   Unlimited websites
        
    -   High power level
        
    -   1 domain name free for the first
        
    -   500 GB SSD storage
        
    -   1,000 email addresses
        
-   1-click CMS
    
    -   WordPress
        
    -   Joomla!
        
    -   Drupal
        
    -   Prestashop
        
-   Database
    
    -   4 x 1 GB databases
        
    -   8 GB Web Cloud Databases
        
-   Security
    
    -   Unlimited free SSL
        
    -   Anti-DDoS protection
        
    -   Anti-virus and anti-spam
        
    -   Daily backups
        
-   Performance
    
    -   99.9% observed availability
        
    -   Guaranteed resources
        
    -   Unlimited traffic
        
    -   Service continuity
        
    -   Boost option to withstand temporary traffic spikes
        
-   Support and additional services
    
    -   Git
        
    -   Standard support
        
    -   SSH access
        
    -   CDN Basic
        

Media Product Card

AI Deploy

Easily deploy machine learning models and applications into production, create your API access points with ease, and make effective predictions.

Order Button

Range Input

0100

Status Message

Activate your project and get €200 in free cloud credit

You are currently in discovery mode. Activate your project to unlock your cloud resources and start building in minutes.

Status Modal
