Empty State

Loading interactive example. Full source is available below.

View example source
Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Text } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { CodeRegular } from "@fluentui/react-icons";
import { Link } from "@coreai-ds/react-components";

interface EmptyStateProps {
    /** Optional Illicon or custom artwork displayed above the message. */
    illustration?: React.ReactNode;
    /** Main empty-state heading. */
    header: React.ReactNode;
    /** Optional supporting text explaining the state or the next step. */
    description?: React.ReactNode;
    /** Optional row of value propositions, metrics, or highlights. */
    metrics?: React.ReactNode;
    /** Optional row of cards, choices, integrations, or detailed content. */
    cards?: React.ReactNode;
    /** Optional row of primary and secondary actions. */
    actions?: React.ReactNode;
    /** Optional supporting links displayed after the actions. */
    links?: React.ReactNode;
}

const useEmptyStateStyles = makeStyles({
    root: {
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        rowGap: tokens.spacingVerticalXL,
        width: '100%',
        minHeight: '400px', // no dimension token; preserves the empty-state canvas height
        paddingBlock: tokens.spacingVerticalXXXL,
        paddingInline: tokens.spacingHorizontalXXL,
        textAlign: 'center',
    },
    illustration: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        color: tokens.colorNeutralForeground3,
        fontSize: tokens.fontSizeHero800,
        lineHeight: 1,
    },
    copy: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        rowGap: tokens.spacingVerticalXS,
        maxWidth: '600px', // no dimension token; matches the Figma copy width
    },
    header: {
        marginBlock: 0,
    },
    description: {
        marginBlock: 0,
    },
    collection: {
        display: 'flex',
        flexWrap: 'wrap',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalL,
        width: '100%',
    },
    actions: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
        width: '100%',
    },
    links: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
    },
});

function EmptyState({ illustration, header, description, metrics, cards, actions, links, }: EmptyStateProps) {
    const styles = useEmptyStateStyles();
    return (<div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (<Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>);
}

export const Default = () => (<EmptyState header="Benefit from Action Artifact" actions={<>
          <Button appearance="primary">Action</Button>
          <Button icon={<CodeRegular />}>Action in code</Button>
        </>} links={<Link href="#">How to make the coolest artifact ever</Link>}/>);

export default Default;

Resources

Implementation

CoreAI 0.12.0; Fluent 9.74.9.

Setup and dependencies.

Copy this template into your app. It is not an npm export. Adapt actions and navigation to your product.

Error State

Loading interactive example. Full source is available below.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Text } from "@coreai-ds/react-components";
import { Illicon } from "@coreai-ds/react-components";
import type { DotGridData } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";

interface EmptyStateProps {
    /** Optional Illicon or custom artwork displayed above the message. */
    illustration?: React.ReactNode;
    /** Main empty-state heading. */
    header: React.ReactNode;
    /** Optional supporting text explaining the state or the next step. */
    description?: React.ReactNode;
    /** Optional row of value propositions, metrics, or highlights. */
    metrics?: React.ReactNode;
    /** Optional row of cards, choices, integrations, or detailed content. */
    cards?: React.ReactNode;
    /** Optional row of primary and secondary actions. */
    actions?: React.ReactNode;
    /** Optional supporting links displayed after the actions. */
    links?: React.ReactNode;
}

const useEmptyStateStyles = makeStyles({
    root: {
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        rowGap: tokens.spacingVerticalXL,
        width: '100%',
        minHeight: '400px', // no dimension token; preserves the empty-state canvas height
        paddingBlock: tokens.spacingVerticalXXXL,
        paddingInline: tokens.spacingHorizontalXXL,
        textAlign: 'center',
    },
    illustration: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        color: tokens.colorNeutralForeground3,
        fontSize: tokens.fontSizeHero800,
        lineHeight: 1,
    },
    copy: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        rowGap: tokens.spacingVerticalXS,
        maxWidth: '600px', // no dimension token; matches the Figma copy width
    },
    header: {
        marginBlock: 0,
    },
    description: {
        marginBlock: 0,
    },
    collection: {
        display: 'flex',
        flexWrap: 'wrap',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalL,
        width: '100%',
    },
    actions: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
        width: '100%',
    },
    links: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
    },
});

function EmptyState({ illustration, header, description, metrics, cards, actions, links, }: EmptyStateProps) {
    const styles = useEmptyStateStyles();
    return (<div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (<Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>);
}

// Generated once with iconToDotGrid at each source icon's native 24 x 24 resolution.
const illustrations = {
    agents: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000011111001110000000',
            '000000111111011111000000',
            '000001111111011011100000',
            '000001111110010011100000',
            '000011111110010011110000',
            '000011111110110111110000',
            '000111111100100111111000',
            '001111111100100111111000',
            '001111111101101111111100',
            '001111111001001111111100',
            '001111111001001111111100',
            '000111111011011111111000',
            '000011110011011111110000',
            '000011110010011111110000',
            '000001110110111111100000',
            '000001100110111111100000',
            '000000111100111111000000',
            '000000011001111110000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    search: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000001111110000000000',
            '000000111111111100000000',
            '000001110000001110000000',
            '000011000000000011000000',
            '000011000000000011000000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000011000000000011000000',
            '000011000000000011000000',
            '000001110000001111000000',
            '000000111111111111100000',
            '000000001111110001110000',
            '000000000000000000111000',
            '000000000000000000011100',
            '000000000000000000001100',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    lock: {
        rows: [
            '000000000000000000000000',
            '000000000011110000000000',
            '000000001111111100000000',
            '000000001100001100000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000001111111111111000000',
            '000001111111111111100000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000001100000110000',
            '000011000001100000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000001111111111111100000',
            '000000111111111111000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    error: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000111111000000000',
            '000000011111111110000000',
            '000001110000000011100000',
            '000011100000000001110000',
            '000011000000000000110000',
            '000110000001100000011000',
            '000100000001100000001000',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000000000000001100',
            '001100000000000000001100',
            '000100000001100000001000',
            '000110000000000000011000',
            '000011000000000000110000',
            '000011100000000001110000',
            '000001110000000011100000',
            '000000011111111110000000',
            '000000000111111000000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
} satisfies Record<string, DotGridData>;

export const ErrorState = () => (<EmptyState illustration={<Illicon data={illustrations.error}/>} header="Something went wrong" description="An error occurred while loading this content." actions={<Button appearance="primary">Try again</Button>}/>);

export default ErrorState;

No Permissions

Loading interactive example. Full source is available below.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Text } from "@coreai-ds/react-components";
import { Illicon } from "@coreai-ds/react-components";
import type { DotGridData } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";

interface EmptyStateProps {
    /** Optional Illicon or custom artwork displayed above the message. */
    illustration?: React.ReactNode;
    /** Main empty-state heading. */
    header: React.ReactNode;
    /** Optional supporting text explaining the state or the next step. */
    description?: React.ReactNode;
    /** Optional row of value propositions, metrics, or highlights. */
    metrics?: React.ReactNode;
    /** Optional row of cards, choices, integrations, or detailed content. */
    cards?: React.ReactNode;
    /** Optional row of primary and secondary actions. */
    actions?: React.ReactNode;
    /** Optional supporting links displayed after the actions. */
    links?: React.ReactNode;
}

const useEmptyStateStyles = makeStyles({
    root: {
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        rowGap: tokens.spacingVerticalXL,
        width: '100%',
        minHeight: '400px', // no dimension token; preserves the empty-state canvas height
        paddingBlock: tokens.spacingVerticalXXXL,
        paddingInline: tokens.spacingHorizontalXXL,
        textAlign: 'center',
    },
    illustration: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        color: tokens.colorNeutralForeground3,
        fontSize: tokens.fontSizeHero800,
        lineHeight: 1,
    },
    copy: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        rowGap: tokens.spacingVerticalXS,
        maxWidth: '600px', // no dimension token; matches the Figma copy width
    },
    header: {
        marginBlock: 0,
    },
    description: {
        marginBlock: 0,
    },
    collection: {
        display: 'flex',
        flexWrap: 'wrap',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalL,
        width: '100%',
    },
    actions: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
        width: '100%',
    },
    links: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
    },
});

function EmptyState({ illustration, header, description, metrics, cards, actions, links, }: EmptyStateProps) {
    const styles = useEmptyStateStyles();
    return (<div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (<Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>);
}

// Generated once with iconToDotGrid at each source icon's native 24 x 24 resolution.
const illustrations = {
    agents: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000011111001110000000',
            '000000111111011111000000',
            '000001111111011011100000',
            '000001111110010011100000',
            '000011111110010011110000',
            '000011111110110111110000',
            '000111111100100111111000',
            '001111111100100111111000',
            '001111111101101111111100',
            '001111111001001111111100',
            '001111111001001111111100',
            '000111111011011111111000',
            '000011110011011111110000',
            '000011110010011111110000',
            '000001110110111111100000',
            '000001100110111111100000',
            '000000111100111111000000',
            '000000011001111110000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    search: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000001111110000000000',
            '000000111111111100000000',
            '000001110000001110000000',
            '000011000000000011000000',
            '000011000000000011000000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000011000000000011000000',
            '000011000000000011000000',
            '000001110000001111000000',
            '000000111111111111100000',
            '000000001111110001110000',
            '000000000000000000111000',
            '000000000000000000011100',
            '000000000000000000001100',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    lock: {
        rows: [
            '000000000000000000000000',
            '000000000011110000000000',
            '000000001111111100000000',
            '000000001100001100000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000001111111111111000000',
            '000001111111111111100000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000001100000110000',
            '000011000001100000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000001111111111111100000',
            '000000111111111111000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    error: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000111111000000000',
            '000000011111111110000000',
            '000001110000000011100000',
            '000011100000000001110000',
            '000011000000000000110000',
            '000110000001100000011000',
            '000100000001100000001000',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000000000000001100',
            '001100000000000000001100',
            '000100000001100000001000',
            '000110000000000000011000',
            '000011000000000000110000',
            '000011100000000001110000',
            '000001110000000011100000',
            '000000011111111110000000',
            '000000000111111000000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
} satisfies Record<string, DotGridData>;

export const NoPermissions = () => (<EmptyState illustration={<Illicon data={illustrations.lock}/>} header="Access required" description="You don't have permission to view this content." actions={<Button appearance="secondary">Request access</Button>}/>);

export default NoPermissions;

No Results

Loading interactive example. Full source is available below.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Text } from "@coreai-ds/react-components";
import { Illicon } from "@coreai-ds/react-components";
import type { DotGridData } from "@coreai-ds/react-components";

interface EmptyStateProps {
    /** Optional Illicon or custom artwork displayed above the message. */
    illustration?: React.ReactNode;
    /** Main empty-state heading. */
    header: React.ReactNode;
    /** Optional supporting text explaining the state or the next step. */
    description?: React.ReactNode;
    /** Optional row of value propositions, metrics, or highlights. */
    metrics?: React.ReactNode;
    /** Optional row of cards, choices, integrations, or detailed content. */
    cards?: React.ReactNode;
    /** Optional row of primary and secondary actions. */
    actions?: React.ReactNode;
    /** Optional supporting links displayed after the actions. */
    links?: React.ReactNode;
}

const useEmptyStateStyles = makeStyles({
    root: {
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        rowGap: tokens.spacingVerticalXL,
        width: '100%',
        minHeight: '400px', // no dimension token; preserves the empty-state canvas height
        paddingBlock: tokens.spacingVerticalXXXL,
        paddingInline: tokens.spacingHorizontalXXL,
        textAlign: 'center',
    },
    illustration: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        color: tokens.colorNeutralForeground3,
        fontSize: tokens.fontSizeHero800,
        lineHeight: 1,
    },
    copy: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        rowGap: tokens.spacingVerticalXS,
        maxWidth: '600px', // no dimension token; matches the Figma copy width
    },
    header: {
        marginBlock: 0,
    },
    description: {
        marginBlock: 0,
    },
    collection: {
        display: 'flex',
        flexWrap: 'wrap',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalL,
        width: '100%',
    },
    actions: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
        width: '100%',
    },
    links: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
    },
});

function EmptyState({ illustration, header, description, metrics, cards, actions, links, }: EmptyStateProps) {
    const styles = useEmptyStateStyles();
    return (<div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (<Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>);
}

// Generated once with iconToDotGrid at each source icon's native 24 x 24 resolution.
const illustrations = {
    agents: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000011111001110000000',
            '000000111111011111000000',
            '000001111111011011100000',
            '000001111110010011100000',
            '000011111110010011110000',
            '000011111110110111110000',
            '000111111100100111111000',
            '001111111100100111111000',
            '001111111101101111111100',
            '001111111001001111111100',
            '001111111001001111111100',
            '000111111011011111111000',
            '000011110011011111110000',
            '000011110010011111110000',
            '000001110110111111100000',
            '000001100110111111100000',
            '000000111100111111000000',
            '000000011001111110000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    search: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000001111110000000000',
            '000000111111111100000000',
            '000001110000001110000000',
            '000011000000000011000000',
            '000011000000000011000000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000011000000000011000000',
            '000011000000000011000000',
            '000001110000001111000000',
            '000000111111111111100000',
            '000000001111110001110000',
            '000000000000000000111000',
            '000000000000000000011100',
            '000000000000000000001100',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    lock: {
        rows: [
            '000000000000000000000000',
            '000000000011110000000000',
            '000000001111111100000000',
            '000000001100001100000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000001111111111111000000',
            '000001111111111111100000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000001100000110000',
            '000011000001100000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000001111111111111100000',
            '000000111111111111000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    error: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000111111000000000',
            '000000011111111110000000',
            '000001110000000011100000',
            '000011100000000001110000',
            '000011000000000000110000',
            '000110000001100000011000',
            '000100000001100000001000',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000000000000001100',
            '001100000000000000001100',
            '000100000001100000001000',
            '000110000000000000011000',
            '000011000000000000110000',
            '000011100000000001110000',
            '000001110000000011100000',
            '000000011111111110000000',
            '000000000111111000000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
} satisfies Record<string, DotGridData>;

export const NoResults = () => (<EmptyState illustration={<Illicon data={illustrations.search}/>} header="No results found" description="Try adjusting your search or filters to find what you're looking for."/>);

export default NoResults;

With Action Cards

Loading interactive example. Full source is available below.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { Link } from "@coreai-ds/react-components";
import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Text } from "@coreai-ds/react-components";
import { Card } from "@coreai-ds/react-components";
import { CardHeader } from "@coreai-ds/react-components";
import { CardFooter } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { CodeRegular } from "@fluentui/react-icons";

interface EmptyStateProps {
    /** Optional Illicon or custom artwork displayed above the message. */
    illustration?: React.ReactNode;
    /** Main empty-state heading. */
    header: React.ReactNode;
    /** Optional supporting text explaining the state or the next step. */
    description?: React.ReactNode;
    /** Optional row of value propositions, metrics, or highlights. */
    metrics?: React.ReactNode;
    /** Optional row of cards, choices, integrations, or detailed content. */
    cards?: React.ReactNode;
    /** Optional row of primary and secondary actions. */
    actions?: React.ReactNode;
    /** Optional supporting links displayed after the actions. */
    links?: React.ReactNode;
}

const useEmptyStateStyles = makeStyles({
    root: {
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        rowGap: tokens.spacingVerticalXL,
        width: '100%',
        minHeight: '400px', // no dimension token; preserves the empty-state canvas height
        paddingBlock: tokens.spacingVerticalXXXL,
        paddingInline: tokens.spacingHorizontalXXL,
        textAlign: 'center',
    },
    illustration: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        color: tokens.colorNeutralForeground3,
        fontSize: tokens.fontSizeHero800,
        lineHeight: 1,
    },
    copy: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        rowGap: tokens.spacingVerticalXS,
        maxWidth: '600px', // no dimension token; matches the Figma copy width
    },
    header: {
        marginBlock: 0,
    },
    description: {
        marginBlock: 0,
    },
    collection: {
        display: 'flex',
        flexWrap: 'wrap',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalL,
        width: '100%',
    },
    actions: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
        width: '100%',
    },
    links: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
    },
});

function EmptyState({ illustration, header, description, metrics, cards, actions, links, }: EmptyStateProps) {
    const styles = useEmptyStateStyles();
    return (<div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (<Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>);
}

export const WithActionCards = () => {
    const actions = [
        {
            heading: 'Connect data sources',
            description: 'Integrate with your enterprise data from supported sources like Fabric, Cosmos, Blob storage, and more.',
            action: 'Connect',
        },
        {
            heading: 'Upload files',
            description: 'Add files that can provide context for any agent in your project.',
            action: 'Upload',
        },
        {
            heading: 'Integrate Microsoft IQ',
            description: (<>
            Set up IQ integration for full business intelligence in your agents.{' '}
            <Link href="#">What can Work IQ do?</Link>
          </>),
            action: 'Integrate',
        },
    ];
    const styles = /* css */ `
      .empty-state-actions-example__card {
        box-sizing: border-box;
        display: flex;
        flex: 0 0 auto;
        width: 256px;
        height: 148px;
        text-align: left;
      }

      .empty-state-actions-example__header {
        flex: 1;
        align-content: start;
        row-gap: var(--spacingVerticalXS);
      }

      .empty-state-actions-example__footer {
        gap: var(--spacingHorizontalXS);
      }
    `;
    return (<>
        <style>{styles}</style>
        <EmptyState header="Benefit from Action Artifact" cards={<>
              {actions.map(({ heading, description, action }) => (<Card className="empty-state-actions-example__card" key={heading}>
                  <CardHeader className="empty-state-actions-example__header" header={<Text preset="heading5" foreground={1}>
                        {heading}
                      </Text>} description={<Text preset="bodyS">{description}</Text>}/>
                  <CardFooter className="empty-state-actions-example__footer">
                    <Button>{action}</Button>
                    <Button appearance="transparent" icon={<CodeRegular />}>
                      Use code
                    </Button>
                  </CardFooter>
                </Card>))}
            </>} links={<Link href="#">How to make the coolest artifact ever</Link>}/>
      </>);
};

export default WithActionCards;

With Choice Cards

Loading interactive example. Full source is available below.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { useId } from "@coreai-ds/react-components";
import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Text } from "@coreai-ds/react-components";
import { RadioGroup } from "@coreai-ds/react-components";
import { Card } from "@coreai-ds/react-components";
import { Radio } from "@coreai-ds/react-components";
import { CardHeader } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { CodeRegular } from "@fluentui/react-icons";
import { Link } from "@coreai-ds/react-components";

interface EmptyStateProps {
    /** Optional Illicon or custom artwork displayed above the message. */
    illustration?: React.ReactNode;
    /** Main empty-state heading. */
    header: React.ReactNode;
    /** Optional supporting text explaining the state or the next step. */
    description?: React.ReactNode;
    /** Optional row of value propositions, metrics, or highlights. */
    metrics?: React.ReactNode;
    /** Optional row of cards, choices, integrations, or detailed content. */
    cards?: React.ReactNode;
    /** Optional row of primary and secondary actions. */
    actions?: React.ReactNode;
    /** Optional supporting links displayed after the actions. */
    links?: React.ReactNode;
}

const useEmptyStateStyles = makeStyles({
    root: {
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        rowGap: tokens.spacingVerticalXL,
        width: '100%',
        minHeight: '400px', // no dimension token; preserves the empty-state canvas height
        paddingBlock: tokens.spacingVerticalXXXL,
        paddingInline: tokens.spacingHorizontalXXL,
        textAlign: 'center',
    },
    illustration: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        color: tokens.colorNeutralForeground3,
        fontSize: tokens.fontSizeHero800,
        lineHeight: 1,
    },
    copy: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        rowGap: tokens.spacingVerticalXS,
        maxWidth: '600px', // no dimension token; matches the Figma copy width
    },
    header: {
        marginBlock: 0,
    },
    description: {
        marginBlock: 0,
    },
    collection: {
        display: 'flex',
        flexWrap: 'wrap',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalL,
        width: '100%',
    },
    actions: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
        width: '100%',
    },
    links: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
    },
});

function EmptyState({ illustration, header, description, metrics, cards, actions, links, }: EmptyStateProps) {
    const styles = useEmptyStateStyles();
    return (<div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (<Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>);
}

export const WithChoiceCards = () => {
    const options = [
        {
            value: 'quickstart',
            heading: 'Quickstart',
            description: 'Build from a guided starting point.',
        },
        {
            value: 'specific',
            heading: 'Specific outcome',
            description: 'Create a focused solution for one task.',
        },
        {
            value: 'advanced',
            heading: 'Advanced setup',
            description: 'Configure each part of the solution.',
        },
    ];
    const idPrefix = useId('empty-state-choice-title');
    const groupLabelId = useId('empty-state-choice-label');
    const [selectedValue, setSelectedValue] = React.useState('quickstart');
    const [submittedValue, setSubmittedValue] = React.useState('');
    const onChange = React.useCallback((_: React.FormEvent<HTMLDivElement>, data: {
        value: string;
    }) => setSelectedValue(data.value), []);
    const onSelectionChange = React.useCallback((value: string) => () => setSelectedValue(value), []);
    const onSubmit = React.useCallback((event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        setSubmittedValue(selectedValue);
    }, [selectedValue]);
    const styles = /* css */ `
      .empty-state-choice-example {
        width: 100%;
      }

      .empty-state-choice-example__choices {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: var(--spacingVerticalS);
        width: 100%;
      }

      .empty-state-choice-example__group {
        display: flex;
        flex: 1 1 100%;
        justify-content: center;
        gap: var(--spacingHorizontalL);
        width: 100%;
        max-width: 800px;
      }

      .empty-state-choice-example__card {
        box-sizing: border-box;
        display: flex;
        flex: 0 0 auto;
        width: 256px;
        min-height: 84px;
        text-align: left;
      }

      .empty-state-choice-example__header {
        flex: 1;
        min-width: 0;
        row-gap: var(--spacingVerticalXXS);
      }

      @media (max-width: 848px) {
        .empty-state-choice-example__group {
          flex-direction: column;
          align-items: center;
        }
      }
    `;
    return (<>
        <style>{styles}</style>
        <form className="empty-state-choice-example" onSubmit={onSubmit}>
          <EmptyState header="What would you like to create?" description="Choose the outcome that best matches your goal." cards={<div className="empty-state-choice-example__choices">
                <Text id={groupLabelId} weight="semibold">
                  Creation method
                </Text>
                <RadioGroup name="creation-method" value={selectedValue} onChange={onChange} layout="horizontal" className="empty-state-choice-example__group" aria-labelledby={groupLabelId}>
                  {options.map(({ value, heading, description }) => {
                const titleId = `${idPrefix}-${value}`;
                return (<Card key={value} className="empty-state-choice-example__card" floatingActionPosition="left" floatingAction={<Radio value={value} aria-labelledby={titleId}/>} selected={selectedValue === value} onSelectionChange={onSelectionChange(value)}>
                        <CardHeader className="empty-state-choice-example__header" header={<Text id={titleId} preset="heading5" foreground={1}>
                              {heading}
                            </Text>} description={<Text preset="bodyS">{description}</Text>}/>
                      </Card>);
            })}
                </RadioGroup>
              </div>} actions={<>
                <Button appearance="primary" type="submit">
                  Continue
                </Button>
                <Button type="button" icon={<CodeRegular />}>
                  Action in code
                </Button>
                {submittedValue ? (<Text role="status">Selected method: {submittedValue}</Text>) : null}
              </>} links={<Link href="#">Compare creation methods</Link>}/>
        </form>
      </>);
};

export default WithChoiceCards;

With Detail Cards

Loading interactive example. Full source is available below.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { Text } from "@coreai-ds/react-components";
import { ArrowUpRegular } from "@fluentui/react-icons";
import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Card } from "@coreai-ds/react-components";
import { CardHeader } from "@coreai-ds/react-components";
import { Badge } from "@coreai-ds/react-components";
import { Divider } from "@coreai-ds/react-components";
import { mergeClasses } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { CodeRegular } from "@fluentui/react-icons";
import { Link } from "@coreai-ds/react-components";

interface EmptyStateProps {
    /** Optional Illicon or custom artwork displayed above the message. */
    illustration?: React.ReactNode;
    /** Main empty-state heading. */
    header: React.ReactNode;
    /** Optional supporting text explaining the state or the next step. */
    description?: React.ReactNode;
    /** Optional row of value propositions, metrics, or highlights. */
    metrics?: React.ReactNode;
    /** Optional row of cards, choices, integrations, or detailed content. */
    cards?: React.ReactNode;
    /** Optional row of primary and secondary actions. */
    actions?: React.ReactNode;
    /** Optional supporting links displayed after the actions. */
    links?: React.ReactNode;
}

const useEmptyStateStyles = makeStyles({
    root: {
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        rowGap: tokens.spacingVerticalXL,
        width: '100%',
        minHeight: '400px', // no dimension token; preserves the empty-state canvas height
        paddingBlock: tokens.spacingVerticalXXXL,
        paddingInline: tokens.spacingHorizontalXXL,
        textAlign: 'center',
    },
    illustration: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        color: tokens.colorNeutralForeground3,
        fontSize: tokens.fontSizeHero800,
        lineHeight: 1,
    },
    copy: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        rowGap: tokens.spacingVerticalXS,
        maxWidth: '600px', // no dimension token; matches the Figma copy width
    },
    header: {
        marginBlock: 0,
    },
    description: {
        marginBlock: 0,
    },
    collection: {
        display: 'flex',
        flexWrap: 'wrap',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalL,
        width: '100%',
    },
    actions: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
        width: '100%',
    },
    links: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
    },
});

function EmptyState({ illustration, header, description, metrics, cards, actions, links, }: EmptyStateProps) {
    const styles = useEmptyStateStyles();
    return (<div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (<Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>);
}

type DetailsItem = {
    key: React.Key;
    label: React.ReactNode;
    /** The value to show. If there is no value, leave this detail out of the list. */
    value: React.ReactNode;
};

interface DetailsListProps extends Omit<React.ComponentPropsWithoutRef<'dl'>, 'children'> {
    items: DetailsItem[];
    itemClassName?: string;
    labelClassName?: string;
    valueClassName?: string;
}

// ─── Styles ──────────────────────────────────────────────────────────────────
const useStyles = makeStyles({
    // ── Root container ─────────────────────────────────────────────────────────
    root: {
        display: 'flex',
        flexDirection: 'column',
        rowGap: tokens.spacingVerticalL,
        margin: 0,
        padding: 0,
        width: '100%',
    },
    // ── Row layout ─────────────────────────────────────────────────────────────
    item: {
        display: 'flex',
        flexDirection: 'column',
        rowGap: tokens.spacingVerticalXXS,
    },
    // ── Label / value text ────────────────────────────────────────────────────
    label: {
        margin: 0,
    },
    value: {
        margin: 0,
    },
});

// ─── Component ────────────────────────────────────────────────────────────────
function DetailsList({ items, className, itemClassName, labelClassName, valueClassName, ...rest }: DetailsListProps) {
    const styles = useStyles();
    return (<dl {...rest} className={mergeClasses('fui-DetailsList', styles.root, className)}>
      {items.map((item) => (<div key={item.key} className={mergeClasses('fui-DetailsList__item', styles.item, itemClassName)}>
          <dt className={mergeClasses('fui-DetailsList__label', styles.label, labelClassName)}>
            {typeof item.label === 'string' || typeof item.label === 'number' ? (<Text preset="bodySStrong" foreground={2}>
                {item.label}
              </Text>) : (item.label)}
          </dt>
          <dd className={mergeClasses('fui-DetailsList__value', styles.value, valueClassName)}>
            {typeof item.value === 'string' || typeof item.value === 'number' ? (<Text preset="body" foreground={1}>
                {item.value}
              </Text>) : (item.value)}
          </dd>
        </div>))}
    </dl>);
}

export const WithDetailCards = () => {
    const options = [
        { label: 'Before', isUpsell: false },
        { label: 'Upsell', isUpsell: true },
    ].map((option) => ({
        ...option,
        items: ['first', 'second', 'third'].map((key) => ({
            key,
            label: <Text preset="bodyS">Primary text slot</Text>,
            value: (<span className="empty-state-details-example__value">
            <Text preset="bodyS" foreground={3}>
              00:00 AM
            </Text>
            {option.isUpsell && (<Text preset="bodyS" className="empty-state-details-example__improvement" aria-label="97% increase">
                <ArrowUpRegular aria-hidden="true"/>
                97%
              </Text>)}
          </span>),
        })),
    }));
    const styles = /* css */ `
      .empty-state-details-example__card {
        box-sizing: border-box;
        flex: 0 0 auto;
        width: 392px;
        max-width: 100%;
        height: 197px;
        gap: var(--spacingVerticalS);
        padding: var(--spacingVerticalL) var(--spacingHorizontalL);
        text-align: left;
      }

      .empty-state-details-example__list {
        row-gap: var(--spacingVerticalS);
        line-height: var(--lineHeightBase200);
      }

      .empty-state-details-example__card--upsell {
        border-color: var(--colorBrandStroke1);
      }

      .empty-state-details-example__value {
        display: flex;
        align-items: center;
        gap: var(--spacingHorizontalM);
      }

      .empty-state-details-example__improvement {
        display: inline-flex;
        align-items: center;
        gap: var(--spacingHorizontalXXS);
        color: var(--colorStatusSuccessForeground1);
      }

      .empty-state-details-example__row {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        gap: var(--spacingHorizontalM);
      }

      .empty-state-details-example__header {
        row-gap: var(--spacingVerticalXXS);
      }

      .empty-state-details-example__option {
        white-space: nowrap;
      }
    `;
    return (<>
        <style>{styles}</style>
        <EmptyState header="Benefit from Action Artifact" cards={<>
              {options.map(({ label, isUpsell, items }) => (<Card className={`empty-state-details-example__card${isUpsell ? ' empty-state-details-example__card--upsell' : ''}`} key={label}>
                  <CardHeader className="empty-state-details-example__header" header={<Text preset="heading5" foreground={1}>
                        Card title
                      </Text>} description={<Text preset="bodyS">Additional metadata</Text>} action={<Badge appearance="tint" color={isUpsell ? 'brand' : 'subtle'} shape="rounded" className="empty-state-details-example__option">
                        {label}
                      </Badge>}/>
                  <Divider />
                  <DetailsList items={items} className="empty-state-details-example__list" itemClassName="empty-state-details-example__row"/>
                </Card>))}
            </>} actions={<>
              <Button appearance="primary">Action</Button>
              <Button icon={<CodeRegular />}>Action in code</Button>
            </>} links={<Link href="#">How to make the coolest artifact ever</Link>}/>
      </>);
};

export default WithDetailCards;

Empty State with Illustration

Loading interactive example. Full source is available below.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Text } from "@coreai-ds/react-components";
import { Illicon } from "@coreai-ds/react-components";
import type { DotGridData } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { CodeRegular } from "@fluentui/react-icons";
import { Link } from "@coreai-ds/react-components";

interface EmptyStateProps {
    /** Optional Illicon or custom artwork displayed above the message. */
    illustration?: React.ReactNode;
    /** Main empty-state heading. */
    header: React.ReactNode;
    /** Optional supporting text explaining the state or the next step. */
    description?: React.ReactNode;
    /** Optional row of value propositions, metrics, or highlights. */
    metrics?: React.ReactNode;
    /** Optional row of cards, choices, integrations, or detailed content. */
    cards?: React.ReactNode;
    /** Optional row of primary and secondary actions. */
    actions?: React.ReactNode;
    /** Optional supporting links displayed after the actions. */
    links?: React.ReactNode;
}

const useEmptyStateStyles = makeStyles({
    root: {
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        rowGap: tokens.spacingVerticalXL,
        width: '100%',
        minHeight: '400px', // no dimension token; preserves the empty-state canvas height
        paddingBlock: tokens.spacingVerticalXXXL,
        paddingInline: tokens.spacingHorizontalXXL,
        textAlign: 'center',
    },
    illustration: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        color: tokens.colorNeutralForeground3,
        fontSize: tokens.fontSizeHero800,
        lineHeight: 1,
    },
    copy: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        rowGap: tokens.spacingVerticalXS,
        maxWidth: '600px', // no dimension token; matches the Figma copy width
    },
    header: {
        marginBlock: 0,
    },
    description: {
        marginBlock: 0,
    },
    collection: {
        display: 'flex',
        flexWrap: 'wrap',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalL,
        width: '100%',
    },
    actions: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
        width: '100%',
    },
    links: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
    },
});

function EmptyState({ illustration, header, description, metrics, cards, actions, links, }: EmptyStateProps) {
    const styles = useEmptyStateStyles();
    return (<div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (<Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>);
}

// Generated once with iconToDotGrid at each source icon's native 24 x 24 resolution.
const illustrations = {
    agents: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000011111001110000000',
            '000000111111011111000000',
            '000001111111011011100000',
            '000001111110010011100000',
            '000011111110010011110000',
            '000011111110110111110000',
            '000111111100100111111000',
            '001111111100100111111000',
            '001111111101101111111100',
            '001111111001001111111100',
            '001111111001001111111100',
            '000111111011011111111000',
            '000011110011011111110000',
            '000011110010011111110000',
            '000001110110111111100000',
            '000001100110111111100000',
            '000000111100111111000000',
            '000000011001111110000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    search: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000000000000000000',
            '000000001111110000000000',
            '000000111111111100000000',
            '000001110000001110000000',
            '000011000000000011000000',
            '000011000000000011000000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000110000000000001100000',
            '000011000000000011000000',
            '000011000000000011000000',
            '000001110000001111000000',
            '000000111111111111100000',
            '000000001111110001110000',
            '000000000000000000111000',
            '000000000000000000011100',
            '000000000000000000001100',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    lock: {
        rows: [
            '000000000000000000000000',
            '000000000011110000000000',
            '000000001111111100000000',
            '000000001100001100000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000000011000000110000000',
            '000001111111111111000000',
            '000001111111111111100000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000001100000110000',
            '000011000001100000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000011000000000000110000',
            '000001111111111111100000',
            '000000111111111111000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
    error: {
        rows: [
            '000000000000000000000000',
            '000000000000000000000000',
            '000000000111111000000000',
            '000000011111111110000000',
            '000001110000000011100000',
            '000011100000000001110000',
            '000011000000000000110000',
            '000110000001100000011000',
            '000100000001100000001000',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000001100000001100',
            '001100000000000000001100',
            '001100000000000000001100',
            '000100000001100000001000',
            '000110000000000000011000',
            '000011000000000000110000',
            '000011100000000001110000',
            '000001110000000011100000',
            '000000011111111110000000',
            '000000000111111000000000',
            '000000000000000000000000',
            '000000000000000000000000',
        ],
    },
} satisfies Record<string, DotGridData>;

export const WithIllustration = () => (<EmptyState illustration={<Illicon data={illustrations.agents}/>} header="Benefit from Action Artifact" description="Disambiguate with text or exclude entirely." actions={<>
          <Button appearance="primary">Create agent</Button>
          <Button icon={<CodeRegular />}>Code</Button>
        </>} links={<Link href="#">How to make the coolest artifact ever</Link>}/>);

export default WithIllustration;

With Metrics

Loading interactive example. Full source is available below.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Text } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { CodeRegular } from "@fluentui/react-icons";
import { Link } from "@coreai-ds/react-components";

interface EmptyStateProps {
    /** Optional Illicon or custom artwork displayed above the message. */
    illustration?: React.ReactNode;
    /** Main empty-state heading. */
    header: React.ReactNode;
    /** Optional supporting text explaining the state or the next step. */
    description?: React.ReactNode;
    /** Optional row of value propositions, metrics, or highlights. */
    metrics?: React.ReactNode;
    /** Optional row of cards, choices, integrations, or detailed content. */
    cards?: React.ReactNode;
    /** Optional row of primary and secondary actions. */
    actions?: React.ReactNode;
    /** Optional supporting links displayed after the actions. */
    links?: React.ReactNode;
}

const useEmptyStateStyles = makeStyles({
    root: {
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        rowGap: tokens.spacingVerticalXL,
        width: '100%',
        minHeight: '400px', // no dimension token; preserves the empty-state canvas height
        paddingBlock: tokens.spacingVerticalXXXL,
        paddingInline: tokens.spacingHorizontalXXL,
        textAlign: 'center',
    },
    illustration: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        color: tokens.colorNeutralForeground3,
        fontSize: tokens.fontSizeHero800,
        lineHeight: 1,
    },
    copy: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        rowGap: tokens.spacingVerticalXS,
        maxWidth: '600px', // no dimension token; matches the Figma copy width
    },
    header: {
        marginBlock: 0,
    },
    description: {
        marginBlock: 0,
    },
    collection: {
        display: 'flex',
        flexWrap: 'wrap',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalL,
        width: '100%',
    },
    actions: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
        width: '100%',
    },
    links: {
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        justifyContent: 'center',
        gap: tokens.spacingHorizontalS,
    },
});

function EmptyState({ illustration, header, description, metrics, cards, actions, links, }: EmptyStateProps) {
    const styles = useEmptyStateStyles();
    return (<div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (<Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>);
}

export const WithMetrics = () => {
    const metrics = ['Fast setup', 'Flexible tools', 'Built-in guidance'];
    const styles = /* css */ `
      .empty-state-metrics-example__metric {
        box-sizing: border-box;
        display: flex;
        flex: 0 0 200px;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 200px;
        height: 72px;
        padding: var(--spacingVerticalM) var(--spacingHorizontalL);
        border-radius: var(--borderRadiusMedium);
        background: var(--colorNeutralBackground2);
        text-align: center;
      }

    `;
    return (<>
        <style>{styles}</style>
        <EmptyState header="Benefit from Action Artifact" description="Disambiguate with text or exclude entirely." metrics={<>
              {metrics.map((label) => (<div className="empty-state-metrics-example__metric" key={label}>
                  <Text preset="bodySStrong" style={{ color: tokens.colorPaletteGreenForeground1 }}>
                    Value prop
                  </Text>
                  <Text preset="bodyStrong">{label}</Text>
                  <Text preset="bodyS">Metric label</Text>
                </div>))}
            </>} actions={<>
              <Button appearance="primary">Action</Button>
              <Button icon={<CodeRegular />}>Action in code</Button>
            </>} links={<Link href="#">How to make the coolest artifact ever</Link>}/>
      </>);
};

export default WithMetrics;

Template source: EmptyState.tsx

Copy this file into your app. Do not import this template from the package.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
 * Templates / Empty State
 *
 * Composable empty-state pattern for pages and data surfaces that have no
 * content to display. Supply only the slots needed for the use case, from a
 * compact message through to metrics, cards, actions, and supporting links.
 * Use Illicon with saved grid data for illustrations; the slot also accepts
 * custom artwork without converting it during rendering.
 *
 * Copy-paste starting point — not a published, importable component. Copy
 * this file into your codebase and adjust as needed.
 */

import * as React from 'react';
import { makeStyles, tokens, Text } from '@coreai-ds/react-components';

export const useEmptyStateStyles = makeStyles({
  root: {
    boxSizing: 'border-box',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
    rowGap: tokens.spacingVerticalXL,
    width: '100%',
    minHeight: '400px', // no dimension token; preserves the empty-state canvas height
    paddingBlock: tokens.spacingVerticalXXXL,
    paddingInline: tokens.spacingHorizontalXXL,
    textAlign: 'center',
  },
  illustration: {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    color: tokens.colorNeutralForeground3,
    fontSize: tokens.fontSizeHero800,
    lineHeight: 1,
  },
  copy: {
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    rowGap: tokens.spacingVerticalXS,
    maxWidth: '600px', // no dimension token; matches the Figma copy width
  },
  header: {
    marginBlock: 0,
  },
  description: {
    marginBlock: 0,
  },
  collection: {
    display: 'flex',
    flexWrap: 'wrap',
    justifyContent: 'center',
    gap: tokens.spacingHorizontalL,
    width: '100%',
  },
  actions: {
    display: 'flex',
    flexWrap: 'wrap',
    alignItems: 'center',
    justifyContent: 'center',
    gap: tokens.spacingHorizontalS,
    width: '100%',
  },
  links: {
    display: 'flex',
    flexWrap: 'wrap',
    alignItems: 'center',
    justifyContent: 'center',
    gap: tokens.spacingHorizontalS,
  },
});

export interface EmptyStateProps {
  /** Optional Illicon or custom artwork displayed above the message. */
  illustration?: React.ReactNode;
  /** Main empty-state heading. */
  header: React.ReactNode;
  /** Optional supporting text explaining the state or the next step. */
  description?: React.ReactNode;
  /** Optional row of value propositions, metrics, or highlights. */
  metrics?: React.ReactNode;
  /** Optional row of cards, choices, integrations, or detailed content. */
  cards?: React.ReactNode;
  /** Optional row of primary and secondary actions. */
  actions?: React.ReactNode;
  /** Optional supporting links displayed after the actions. */
  links?: React.ReactNode;
}

export function EmptyState({
  illustration,
  header,
  description,
  metrics,
  cards,
  actions,
  links,
}: EmptyStateProps) {
  const styles = useEmptyStateStyles();
  return (
    <div className={styles.root}>
      {illustration ? <div className={styles.illustration}>{illustration}</div> : null}

      <div className={styles.copy}>
        <Text as="h2" preset="heading4" foreground={1} className={styles.header}>
          {header}
        </Text>
        {description ? (
          <Text as="p" preset="body" foreground={2} className={styles.description}>
            {description}
          </Text>
        ) : null}
      </div>

      {metrics ? <div className={styles.collection}>{metrics}</div> : null}
      {cards ? <div className={styles.collection}>{cards}</div> : null}
      {actions ? <div className={styles.actions}>{actions}</div> : null}
      {links ? <div className={styles.links}>{links}</div> : null}
    </div>
  );
}

Template source: DetailsList.tsx

Copy this file into your app. Do not import this template from the package.

Download source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
 * Templates / Details List
 *
 * Starter scaffold for building a label-value details panel with CoreAI
 * primitives. Keep the semantic <dl>/<dt>/<dd> structure, replace the sample
 * items with your own data, and optionally customize class hooks.
 *
 * Displays label-value pairs in a compact, readable layout.
 * Useful for user profiles, resource metadata, and summary panels.
 *
 * Copy-paste starting point — not an importable component.
 */

import * as React from 'react';
import { makeStyles, mergeClasses, Text, tokens } from '@coreai-ds/react-components';

export type DetailsItem = {
  key: React.Key;
  label: React.ReactNode;
  /** The value to show. If there is no value, leave this detail out of the list. */
  value: React.ReactNode;
};

// ─── Styles ──────────────────────────────────────────────────────────────────

const useStyles = makeStyles({
  // ── Root container ─────────────────────────────────────────────────────────
  root: {
    display: 'flex',
    flexDirection: 'column',
    rowGap: tokens.spacingVerticalL,
    margin: 0,
    padding: 0,
    width: '100%',
  },

  // ── Row layout ─────────────────────────────────────────────────────────────
  item: {
    display: 'flex',
    flexDirection: 'column',
    rowGap: tokens.spacingVerticalXXS,
  },

  // ── Label / value text ────────────────────────────────────────────────────
  label: {
    margin: 0,
  },
  value: {
    margin: 0,
  },
});

export interface DetailsListProps extends Omit<React.ComponentPropsWithoutRef<'dl'>, 'children'> {
  items: DetailsItem[];
  itemClassName?: string;
  labelClassName?: string;
  valueClassName?: string;
}

// ─── Component ────────────────────────────────────────────────────────────────

export function DetailsList({
  items,
  className,
  itemClassName,
  labelClassName,
  valueClassName,
  ...rest
}: DetailsListProps) {
  const styles = useStyles();

  return (
    <dl {...rest} className={mergeClasses('fui-DetailsList', styles.root, className)}>
      {items.map((item) => (
        <div
          key={item.key}
          className={mergeClasses('fui-DetailsList__item', styles.item, itemClassName)}
        >
          <dt className={mergeClasses('fui-DetailsList__label', styles.label, labelClassName)}>
            {typeof item.label === 'string' || typeof item.label === 'number' ? (
              <Text preset="bodySStrong" foreground={2}>
                {item.label}
              </Text>
            ) : (
              item.label
            )}
          </dt>
          <dd className={mergeClasses('fui-DetailsList__value', styles.value, valueClassName)}>
            {typeof item.value === 'string' || typeof item.value === 'number' ? (
              <Text preset="body" foreground={1}>
                {item.value}
              </Text>
            ) : (
              item.value
            )}
          </dd>
        </div>
      ))}
    </dl>
  );
}