Skip to main content
Nexus Design System
GitHub

Show

Show displays its children only above or below a viewport or container width. Use it for responsive visibility instead of toggling nx:hidden classes. To hide content at a width, use Hide.

Visible when the viewport is lg (≥ 64rem) or wider.
Visible when the viewport is narrower than lg.
'use client';

import { Show } from '@/components/nxds/components/show/show';

const box =
  'nx:rounded-md nx:border-default nx:border-border-default nx:p-3 nx:text-foreground';

export default function ShowDefault() {
  return (
    <div className="nx:flex nx:flex-col nx:gap-2">
      <Show above="lg" as="div">
        <div className={box}>
          Visible when the viewport is <strong>lg</strong> (≥ 64rem) or wider.
        </div>
      </Show>
      <Show below="lg" as="div">
        <div className={box}>
          Visible when the viewport is narrower than <strong>lg</strong>.
        </div>
      </Show>
    </div>
  );
}

Installation

npx nxds-cli@latest add show

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import { Show } from '@/components/nxds/components/show/show';

Examples

All axes

Each of the four props shows its content at a different viewport or container width.

<Show above="lg"> — viewport ≥ lg
Shown on wide viewports.
<Show below="lg"> — viewport < lg
Shown on narrow viewports.
<Show containerAbove="md"> — this 600px container ≥ md
Shown because the container is wide.
<Show containerBelow="md"> — this 240px container < md
Shown because the container is narrow.
'use client';

import { Show } from '@/components/nxds/components/show/show';

const box =
  'nx:rounded-md nx:border-default nx:border-border-default nx:p-3 nx:text-foreground';

const label = 'nx:text-muted-foreground nx:mb-1 nx:block';

export default function ShowAllAxes() {
  return (
    <div className="nx:flex nx:flex-col nx:gap-4">
      <div>
        <span className={label}>{'<Show above="lg">'} — viewport ≥ lg</span>
        <Show above="lg" as="div">
          <div className={box}>Shown on wide viewports.</div>
        </Show>
      </div>
      <div>
        <span className={label}>{'<Show below="lg">'} — viewport &lt; lg</span>
        <Show below="lg" as="div">
          <div className={box}>Shown on narrow viewports.</div>
        </Show>
      </div>
      <div
        className="nx:@container nx:rounded-md nx:border-default nx:border-border-default nx:p-2"
        style={{ width: 600 }}
      >
        <span className={label}>
          {'<Show containerAbove="md">'} — this 600px container ≥ md
        </span>
        <Show containerAbove="md" as="div">
          <div className={box}>Shown because the container is wide.</div>
        </Show>
      </div>
      <div
        className="nx:@container nx:rounded-md nx:border-default nx:border-border-default nx:p-2"
        style={{ width: 240 }}
      >
        <span className={label}>
          {'<Show containerBelow="md">'} — this 240px container &lt; md
        </span>
        <Show containerBelow="md" as="div">
          <div className={box}>Shown because the container is narrow.</div>
        </Show>
      </div>
    </div>
  );
}

Props

Show

Show props
PropTypeDefaultDescription
above"sm" | "lg" | "md" | "xl" | "2xl"No default documentedNo description
as"span" | "div""span"

Wrapper tag: span (inline) or div (block) — match the child's layout. Tag-only and non-semantic: display: contents can drop a semantic element's box from the accessibility tree, so put semantics on a child inside <Show> / <Hide> instead.

below"sm" | "lg" | "md" | "xl" | "2xl"No default documentedNo description
containerAbove"sm" | "lg" | "md" | "xl" | "2xl"No default documentedNo description
containerBelow"sm" | "lg" | "md" | "xl" | "2xl"No default documentedNo description

Show displays its children only above or below a viewport or container width. Use it for responsive visibility instead of toggling nx:hidden classes. To hide content at a width, use Hide.

<script setup lang="ts">
import { Show } from '@/components/show';
</script>

<template>
  <div class="nx:flex nx:flex-col nx:gap-2">
    <Show above="lg" as="div">
      <div
        class="nx:rounded-md nx:border-default nx:border-border-default nx:p-3 nx:text-foreground"
      >
        Visible when the viewport is <strong>lg</strong> (≥ 64rem) or wider.
      </div>
    </Show>
    <Show below="lg" as="div">
      <div
        class="nx:rounded-md nx:border-default nx:border-border-default nx:p-3 nx:text-foreground"
      >
        Visible when the viewport is narrower than <strong>lg</strong>.
      </div>
    </Show>
  </div>
</template>

Installation

  1. Copy and paste the following code into your project.

    components/show/Show.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import {
      visibilityClassName,
      type VisibilityProps,
    } from '../../lib/responsive-visibility';
    import { cn } from '../../lib/utils';
    
    export type ShowProps = VisibilityProps & {
      class?: HTMLAttributes['class'];
    };
    
    const props = withDefaults(defineProps<ShowProps>(), { as: 'span' });
    </script>
    
    <template>
      <component
        :is="as"
        data-slot="show"
        :class="cn(props.class, visibilityClassName(props, false))"
      >
        <slot />
      </component>
    </template>
    
    components/show/index.ts
    export { default as Show, type ShowProps } from './Show.vue';
    
    lib/responsive-visibility.ts
    /**
     * Shared internals for the `<Show>` / `<Hide>` responsive-visibility primitives.
     *
     * Visibility toggles between `display: contents` (shown) and `display: none`
     * (hidden) — never `display: block` / `revert`. A `contents` wrapper generates
     * no box of its own, so its children keep their natural flow: inline stays
     * inline, flex/grid children stay flex/grid items. Forcing `block` (or reverting
     * to the element's UA default) would override the wrapper's display and break
     * those layouts.
     *
     * Class strings are full static literals in the maps below: Tailwind only emits
     * a utility it can see as a complete string, so a computed `nx:${bp}:contents`
     * would never ship in the CSS bundle.
     *
     * Container axes use Tailwind's native `@container` scale (`@md` = 28rem), which
     * is smaller than the viewport scale (`md` = 48rem) — a component
     * reads as "md" at a narrower width than the whole viewport. The two axes
     * therefore resolve the same breakpoint name to different rem values.
     */
    
    export type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
    
    /**
     * Exactly one axis must be provided. The `never` siblings make the four axes
     * mutually exclusive at compile time (zero or two+ axes fail to typecheck).
     */
    export type VisibilityAxis =
      | {
          above: Breakpoint;
          below?: never;
          containerAbove?: never;
          containerBelow?: never;
        }
      | {
          above?: never;
          below: Breakpoint;
          containerAbove?: never;
          containerBelow?: never;
        }
      | {
          above?: never;
          below?: never;
          containerAbove: Breakpoint;
          containerBelow?: never;
        }
      | {
          above?: never;
          below?: never;
          containerAbove?: never;
          containerBelow: Breakpoint;
        };
    
    /** Props Show and Hide share; each adds its framework's attributes and children. */
    export type VisibilityProps = {
      /**
       * Wrapper tag: `span` (inline) or `div` (block) — match the child's layout.
       * Tag-only and non-semantic: `display: contents` can
       * drop a semantic element's box from the accessibility tree, so put semantics
       * on a child *inside* `<Show>` / `<Hide>` instead.
       */
      as?: 'span' | 'div';
    } & VisibilityAxis;
    
    /** Loose shape the resolver works with. */
    type AxisValues = {
      above?: Breakpoint;
      below?: Breakpoint;
      containerAbove?: Breakpoint;
      containerBelow?: Breakpoint;
    };
    
    // Shown = `contents`, hidden = `none`. Keyed by breakpoint; each value is a
    // complete, statically-scannable class string (see header for why).
    const VIEWPORT_ABOVE: Record<Breakpoint, string> = {
      sm: 'nx:hidden nx:sm:contents',
      md: 'nx:hidden nx:md:contents',
      lg: 'nx:hidden nx:lg:contents',
      xl: 'nx:hidden nx:xl:contents',
      '2xl': 'nx:hidden nx:2xl:contents',
    };
    
    const VIEWPORT_BELOW: Record<Breakpoint, string> = {
      sm: 'nx:contents nx:sm:hidden',
      md: 'nx:contents nx:md:hidden',
      lg: 'nx:contents nx:lg:hidden',
      xl: 'nx:contents nx:xl:hidden',
      '2xl': 'nx:contents nx:2xl:hidden',
    };
    
    const CONTAINER_ABOVE: Record<Breakpoint, string> = {
      sm: 'nx:hidden nx:@sm:contents',
      md: 'nx:hidden nx:@md:contents',
      lg: 'nx:hidden nx:@lg:contents',
      xl: 'nx:hidden nx:@xl:contents',
      '2xl': 'nx:hidden nx:@2xl:contents',
    };
    
    const CONTAINER_BELOW: Record<Breakpoint, string> = {
      sm: 'nx:contents nx:@sm:hidden',
      md: 'nx:contents nx:@md:hidden',
      lg: 'nx:contents nx:@lg:hidden',
      xl: 'nx:contents nx:@xl:hidden',
      '2xl': 'nx:contents nx:@2xl:hidden',
    };
    
    /**
     * Resolve the visibility class string for the one provided axis. `invert` swaps
     * show/hide so `<Hide>` is `<Show>` with the maps flipped (hide-above === show-below).
     *
     * The string starts with the class `nx:gap-[inherit]`. A `contents` box lays out
     * nothing, so this passes its parent's gap to a child that inherits it, like a
     * toolbar group.
     */
    export function visibilityClassName(axis: AxisValues, invert: boolean): string {
      return `nx:gap-[inherit] ${axisClassName(axis, invert)}`;
    }
    
    function axisClassName(
      { above, below, containerAbove, containerBelow }: AxisValues,
      invert: boolean
    ): string {
      if (above) return (invert ? VIEWPORT_BELOW : VIEWPORT_ABOVE)[above];
      if (below) return (invert ? VIEWPORT_ABOVE : VIEWPORT_BELOW)[below];
      if (containerAbove)
        return (invert ? CONTAINER_BELOW : CONTAINER_ABOVE)[containerAbove];
      if (containerBelow)
        return (invert ? CONTAINER_ABOVE : CONTAINER_BELOW)[containerBelow];
      return '';
    }
    
  2. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Show } from '@/components/show';

Examples

All axes

Each of the four props shows its content at a different viewport or container width.

<script setup lang="ts">
import { Show } from '@/components/show';
</script>

<template>
  <div class="nx:flex nx:flex-col nx:gap-4">
    <div>
      <span class="nx:text-muted-foreground nx:mb-1 nx:block"
        >&lt;Show above="lg"&gt; — viewport ≥ lg</span
      >
      <Show above="lg" as="div">
        <div
          class="nx:rounded-md nx:border-default nx:border-border-default nx:p-3 nx:text-foreground"
        >
          Shown on wide viewports.
        </div>
      </Show>
    </div>
    <div>
      <span class="nx:text-muted-foreground nx:mb-1 nx:block"
        >&lt;Show below="lg"&gt; — viewport &lt; lg</span
      >
      <Show below="lg" as="div">
        <div
          class="nx:rounded-md nx:border-default nx:border-border-default nx:p-3 nx:text-foreground"
        >
          Shown on narrow viewports.
        </div>
      </Show>
    </div>
    <div
      class="nx:@container nx:rounded-md nx:border-default nx:border-border-default nx:p-2"
      style="width: 600px"
    >
      <span class="nx:text-muted-foreground nx:mb-1 nx:block"
        >&lt;Show containerAbove="md"&gt; — this 600px container ≥ md</span
      >
      <Show containerAbove="md" as="div">
        <div
          class="nx:rounded-md nx:border-default nx:border-border-default nx:p-3 nx:text-foreground"
        >
          Shown because the container is wide.
        </div>
      </Show>
    </div>
    <div
      class="nx:@container nx:rounded-md nx:border-default nx:border-border-default nx:p-2"
      style="width: 240px"
    >
      <span class="nx:text-muted-foreground nx:mb-1 nx:block"
        >&lt;Show containerBelow="md"&gt; — this 240px container &lt; md</span
      >
      <Show containerBelow="md" as="div">
        <div
          class="nx:rounded-md nx:border-default nx:border-border-default nx:p-3 nx:text-foreground"
        >
          Shown because the container is narrow.
        </div>
      </Show>
    </div>
  </div>
</template>

Props

Show

Show props
PropTypeDefaultDescription
above"sm" | "lg" | "md" | "xl" | "2xl"No default documentedNo description
as"div" | "span""span"

Wrapper tag: span (inline) or div (block) — match the child's layout. Tag-only and non-semantic: display: contents can drop a semantic element's box from the accessibility tree, so put semantics on a child inside <Show> / <Hide> instead.

below"sm" | "lg" | "md" | "xl" | "2xl"No default documentedNo description
containerAbove"sm" | "lg" | "md" | "xl" | "2xl"No default documentedNo description
containerBelow"sm" | "lg" | "md" | "xl" | "2xl"No default documentedNo description