Ir para o conteúdo

Modal

Diálogo modal baseado no elemento dialog nativo.

Navegadores
Chrome 123+Firefox 121+Safari 17.5+
Nível
0
Tamanho compactado
944 B
Limite de layout forçado
0 ms
Limite do axe
0
Compatível com SSR
Sim
Construído sobre
elements/dialog
Contrato publicado
Ambiente de teste

Experimentar

Modal

Fechado

Controlado

O estado do componente pai controla este modal após a hidratação.

Alterações: 0, estado: fechado

import { Modal } from "@gunkin/uiify";

export default function Example() {
  return (
    <div className="">
      <Modal
        id="settings-dialog"
        trigger="Open settings"
        title="Settings"
        description="Update your profile preferences."
      />
    </div>
  );
}
/* Native dialog surfaces. */

@layer components {
  :where(
    [data-uiify-dialog][data-part="content"],
    [data-uiify-modal][data-part="content"],
    [data-uiify-alert-modal][data-part="content"],
    [data-uiify-drawer][data-part="content"]
  ) {
    margin: auto;
    inline-size: min(28rem, calc(100vw - 2 * var(--space-lg)));
    padding: var(--space-xl);
    border: var(--border-width) solid var(--border);

    /* One-off: the modal is the highest-elevation surface in the 2a/2b
       handoff (uiify Components Kit.dc.html), rounder than the --radius-lg
       card scale (12px) that every other bordered container uses. */
    border-radius: var(--radius-xl);
    corner-shape: squircle;
    background-color: var(--popover);
    color: var(--popover-foreground);
    box-shadow: var(--shadow-modal);
  }

  :where(
    [data-uiify-dialog][data-part="content"],
    [data-uiify-modal][data-part="content"],
    [data-uiify-alert-modal][data-part="content"],
    [data-uiify-drawer][data-part="content"]
  )::backdrop {
    background-color: rgb(0 0 0 / 45%);
  }

  :where([data-uiify-dialog], [data-uiify-modal], [data-uiify-alert-modal], [data-uiify-drawer])
    [data-part="title"] {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: var(--weight-semibold);
    line-height: 1.3;
  }

  :where([data-uiify-dialog], [data-uiify-modal], [data-uiify-alert-modal], [data-uiify-drawer])
    [data-part="description"] {
    margin-block: var(--space-sm) var(--space-lg);
    color: var(--muted-foreground);
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
  }

  @media (prefers-reduced-motion: no-preference) {
    :where(
      [data-uiify-dialog][data-part="content"],
      [data-uiify-modal][data-part="content"],
      [data-uiify-alert-modal][data-part="content"],
      [data-uiify-drawer][data-part="content"]
    ),
    :where(
      [data-uiify-dialog][data-part="content"],
      [data-uiify-modal][data-part="content"],
      [data-uiify-alert-modal][data-part="content"],
      [data-uiify-drawer][data-part="content"]
    )::backdrop {
      transition: var(--transition-presence);
    }

    :where(
      [data-uiify-dialog][data-part="content"],
      [data-uiify-modal][data-part="content"],
      [data-uiify-alert-modal][data-part="content"]
    ):not([open]) {
      opacity: 0;
      scale: var(--presence-scale);
    }

    :where(
        [data-uiify-dialog][data-part="content"],
        [data-uiify-modal][data-part="content"],
        [data-uiify-alert-modal][data-part="content"],
        [data-uiify-drawer][data-part="content"]
      ):not([open])::backdrop {
      opacity: 0;
    }

    @starting-style {
      :where(
        [data-uiify-dialog][data-part="content"],
        [data-uiify-modal][data-part="content"],
        [data-uiify-alert-modal][data-part="content"]
      )[open] {
        opacity: 0;
        scale: var(--presence-scale);
      }

      :where(
          [data-uiify-dialog][data-part="content"],
          [data-uiify-modal][data-part="content"],
          [data-uiify-alert-modal][data-part="content"],
          [data-uiify-drawer][data-part="content"]
        )[open]::backdrop {
        opacity: 0;
      }
    }
  }

  /* Modal anatomy. AlertModal, Drawer and Dialog keep the shared surface above; every rule
     below is scoped to `[data-uiify-modal]`, so their look is unchanged.
     The column layout sits on the base rule, not under [open]: on close `display` stays flex
     through the exit transition (allow-discrete), so the dialog must keep its column until it
     is hidden. Only `display` depends on [open]. */
  [data-uiify-modal][data-part="content"] {
    --uiify-modal-inline-size: 28rem;

    flex-direction: column;
    gap: var(--space-md);
    inline-size: min(var(--uiify-modal-inline-size), calc(100vw - 2 * var(--space-lg)));
    max-block-size: calc(100dvh - 2 * var(--space-lg));
  }

  [data-uiify-modal][data-part="content"][open] {
    display: flex;
  }

  [data-uiify-modal][data-part="content"][data-size="sm"] {
    --uiify-modal-inline-size: 24rem;
  }

  [data-uiify-modal][data-part="content"][data-size="lg"] {
    --uiify-modal-inline-size: 40rem;
  }

  [data-uiify-modal][data-part="content"][data-size="full"] {
    inset: 0;
    margin: 0;
    inline-size: 100%;
    block-size: 100%;
    max-inline-size: none;
    max-block-size: none;
    padding-block: max(var(--space-xl), env(safe-area-inset-top))
      max(var(--space-xl), env(safe-area-inset-bottom));
    padding-inline: max(var(--space-xl), env(safe-area-inset-left))
      max(var(--space-xl), env(safe-area-inset-right));
    border: 0;
    border-radius: 0;
  }

  /* Below 40rem the bottom sheet wins, so align=start only applies from the breakpoint up. */
  @media (width >= 40rem) {
    [data-uiify-modal][data-part="content"][data-align="start"]:not([data-size="full"]) {
      margin-block: var(--space-xl) auto;
    }
  }

  [data-uiify-modal][data-part="header"] {
    position: relative;
    flex: none;
    padding-inline-end: 2.5rem;
  }

  /* Every close button shares one soft look: a filled, borderless surface that steps up on
     hover and press. The header icon is a circle; text closers (footer, body) keep the pill. */
  [data-uiify-modal][data-part="close"][data-uiify-button] {
    border-color: transparent;
    background-color: color-mix(in oklch, var(--foreground) 8%, transparent);
    color: var(--foreground);
  }

  [data-uiify-modal][data-part="close"][data-uiify-button]:hover {
    background-color: color-mix(in oklch, var(--foreground) 14%, transparent);
  }

  [data-uiify-modal][data-part="close"][data-uiify-button]:active {
    background-color: color-mix(in oklch, var(--foreground) 20%, transparent);
  }

  [data-uiify-modal][data-part="header"] > [data-part="close"] {
    position: absolute;
    inset-block-start: 0;
    inset-inline-end: 0;
    display: grid;
    place-items: center;
    inline-size: 2rem;
    block-size: 2rem;
    min-block-size: 0;
    padding: 0;
    border-radius: 999px;
  }

  [data-uiify-modal][data-part="body"] {
    flex: 1 1 auto;
    min-block-size: 0;
    overflow: auto;
    overscroll-behavior: contain;
  }

  [data-uiify-modal][data-part="footer"] {
    display: flex;
    flex: none;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--space-sm);
  }

  @media (pointer: coarse) {
    [data-uiify-modal][data-part="header"] {
      padding-inline-end: 2.75rem;
    }

    [data-uiify-modal][data-part="header"] > [data-part="close"] {
      min-inline-size: 2.75rem;
      min-block-size: 2.75rem;
    }

    [data-uiify-modal][data-part="footer"] > * {
      min-block-size: 2.75rem;
    }
  }

  /* Mobile sheet below 40rem (keep in sync with SHEET_QUERY in enhance/sheet-swipe.ts):
     at least 50dvh and at most 80dvh, so the top 20% of the viewport is always backdrop. */
  [data-uiify-modal][data-part="grabber"] {
    display: none;
    flex: none;
    block-size: 1.5rem;
    margin-block: calc(-1 * var(--space-md)) calc(-1 * var(--space-sm));
    touch-action: none;
  }

  [data-uiify-modal][data-part="grabber"]::after {
    content: "";
    display: block;
    inline-size: 2.25rem;
    block-size: 0.25rem;
    margin-block: 0.5rem 0;
    margin-inline: auto;
    border-radius: 999px;
    background-color: var(--border);
  }

  @media (width < 40rem) {
    [data-uiify-modal][data-part="content"]:not([data-size="full"]) {
      inline-size: 100%;
      max-inline-size: 100%;
      min-block-size: 50dvh;
      max-block-size: 80dvh;
      margin-block: auto 0;
      margin-inline: 0;
      padding-block-end: calc(var(--space-xl) + env(safe-area-inset-bottom));
      border-block-end-width: 0;
      border-end-start-radius: 0;
      border-end-end-radius: 0;
    }

    /* With a ModalBody the body is the scroller, so the rest of the sheet is a swipe surface.
       Without one the dialog itself scrolls and keeps the default touch action; the header and
       grabber stay swipe handles either way (touch-action applies to the element touched). */
    [data-uiify-modal][data-part="content"]:not([data-size="full"]):has(
        > [data-uiify-modal][data-part="body"]
      ) {
      touch-action: none;
    }

    [data-uiify-modal][data-part="content"]:not([data-size="full"])
      [data-uiify-modal][data-part="header"] {
      touch-action: none;
    }

    [data-uiify-modal][data-part="body"] {
      touch-action: pan-y;
    }

    [data-uiify-modal][data-part="content"]:not([data-size="full"]) > [data-part="grabber"] {
      display: block;
    }
  }

  @media (width < 40rem) and (prefers-reduced-motion: no-preference) {
    [data-uiify-modal][data-part="content"]:not([data-size="full"]) {
      transition:
        translate var(--duration-fast) var(--ease-out),
        opacity var(--duration-fast) var(--ease-out),
        display var(--duration-fast) allow-discrete,
        overlay var(--duration-fast) allow-discrete;
    }

    [data-uiify-modal][data-part="content"]:not([data-size="full"], [open]) {
      translate: 0 100%;
      scale: 1;
    }

    @starting-style {
      [data-uiify-modal][data-part="content"]:not([data-size="full"])[open] {
        translate: 0 100%;
        scale: 1;
      }
    }
  }

  /* While a finger drives the sheet (see enhance/sheet-swipe.ts): no transition, position
     from the offset. Removing the attribute hands the position back to the rules above,
     which animates the sheet out or back. */
  [data-uiify-modal][data-part="content"][data-dragging][open]:not([data-size="full"]) {
    transition: none;
    translate: 0 var(--uiify-sheet-offset, 0);
  }

  [data-uiify-modal][data-part="content"][data-backdrop="blur"]::backdrop {
    backdrop-filter: blur(4px);
  }

  [data-uiify-modal][data-part="content"][data-backdrop="none"]::backdrop {
    background-color: transparent;
  }

  /* Nested modals: the child is a DOM descendant of the parent, so CSS can see it. The
     parent's own backdrop keeps dimming the page (one layer); the parent steps back. */
  [data-uiify-modal][data-part="content"] [data-uiify-modal][data-part="content"]::backdrop {
    background-color: transparent;
    backdrop-filter: none;
  }

  [data-uiify-modal][data-part="content"]:has([data-uiify-modal][data-part="content"]:modal) {
    filter: brightness(0.9);
  }

  @media (width >= 40rem) {
    [data-uiify-modal][data-part="content"]:has([data-uiify-modal][data-part="content"]:modal) {
      scale: 0.97;
    }
  }
}
import { Modal } from "@gunkin/uiify";

export default function Example() {
  return (
    <div
      className={
        "[&[data-positioning=native]]:[--uiify-anchor-offset:var(--space-sm)] [&_[data-positioning=native]]:[--uiify" +
        "-anchor-offset:var(--space-sm)] [&[popover]:popover-open:not([data-positioning=native])]:[margin:revert] [&" +
        "_[popover]:popover-open:not([data-positioning=native])]:[margin:revert] [&[data-part=content]]:m-auto [&[da" +
        "ta-part=content]]:[inline-size:min(28rem,_calc(100vw_-_2_*_var(--space-lg)))] [&[data-part=content]]:p-xl [" +
        "&[data-part=content]]:border-[length:var(--border-width)] [&[data-part=content]]:border-solid [&[data-part=" +
        "content]]:border-border [&[data-part=content]]:rounded-xl [&[data-part=content]]:[corner-shape:squircle] [&" +
        "[data-part=content]]:bg-popover [&[data-part=content]]:text-popover-foreground [&[data-part=content]]:shado" +
        "w-modal [&[data-part=content]::backdrop]:[background-color:rgb(0_0_0_/_45%)] [&_[data-part=title]]:m-0 [&_[" +
        "data-part=title]]:text-[length:var(--text-xl)] [&_[data-part=title]]:font-[number:var(--weight-semibold)] [" +
        "&_[data-part=title]]:[line-height:1.3] [&_[data-part=description]]:[margin-block:var(--space-sm)_var(--spac" +
        "e-lg)] [&_[data-part=description]]:text-muted-foreground [&_[data-part=description]]:text-sm [&_[data-part=" +
        "description]]:[line-height:var(--leading-normal)] motion-safe:[&[data-part=content]]:[transition:var(--tran" +
        "sition-presence)] motion-safe:[&[data-part=content]::backdrop]:[transition:var(--transition-presence)] moti" +
        "on-safe:[&[data-part=content]:not([open])]:opacity-0 motion-safe:[&[data-part=content]:not([open])]:[scale:" +
        "var(--presence-scale)] motion-safe:[&[data-part=content]:not([open])::backdrop]:opacity-0 motion-safe:start" +
        "ing:[&[data-part=content][open]]:opacity-0 motion-safe:starting:[&[data-part=content][open]]:[scale:var(--p" +
        "resence-scale)] motion-safe:starting:[&[data-part=content][open]::backdrop]:opacity-0 data-[part=content]:[" +
        "--uiify-modal-inline-size:28rem] data-[part=content]:flex-col data-[part=content]:gap-md data-[part=content" +
        "]:[inline-size:min(var(--uiify-modal-inline-size),_calc(100vw_-_2_*_var(--space-lg)))] data-[part=content]:" +
        "[max-block-size:calc(100dvh_-_2_*_var(--space-lg))] data-[part=content]:[&[open]]:flex data-[part=content]:" +
        "data-[size=sm]:[--uiify-modal-inline-size:24rem] data-[part=content]:data-[size=lg]:[--uiify-modal-inline-s" +
        "ize:40rem] data-[part=content]:data-[size=full]:inset-0 data-[part=content]:data-[size=full]:m-0 data-[part" +
        "=content]:data-[size=full]:[inline-size:100%] data-[part=content]:data-[size=full]:[block-size:100%] data-[" +
        "part=content]:data-[size=full]:[max-inline-size:none] data-[part=content]:data-[size=full]:[max-block-size:" +
        "none] data-[part=content]:data-[size=full]:[padding-block:max(var(--space-xl),_env(safe-area-inset-top))_ma" +
        "x(var(--space-xl),_env(safe-area-inset-bottom))] data-[part=content]:data-[size=full]:[padding-inline:max(v" +
        "ar(--space-xl),_env(safe-area-inset-left))_max(var(--space-xl),_env(safe-area-inset-right))] data-[part=con" +
        "tent]:data-[size=full]:border-0 data-[part=content]:data-[size=full]:border-current data-[part=content]:dat" +
        "a-[size=full]:border-none data-[part=content]:data-[size=full]:rounded-none [@media_(width_>=_40rem)]:data-" +
        "[part=content]:data-[align=start]:[&:not([data-size=full])]:[margin-block:var(--space-xl)_auto] data-[part=" +
        "header]:relative data-[part=header]:flex-none data-[part=header]:[padding-inline-end:2.5rem] data-[part=clo" +
        "se]:data-[uiify-button]:border-transparent data-[part=close]:data-[uiify-button]:[background-color:color-mi" +
        "x(in_oklch,_var(--foreground)_8%,_transparent)] data-[part=close]:data-[uiify-button]:text-foreground data-" +
        "[part=close]:data-[uiify-button]:hover:[background-color:color-mix(in_oklch,_var(--foreground)_14%,_transpa" +
        "rent)] data-[part=close]:data-[uiify-button]:active:[background-color:color-mix(in_oklch,_var(--foreground)" +
        "_20%,_transparent)] [&[data-part=header]>[data-part=close]]:absolute [&[data-part=header]>[data-part=close]" +
        "]:[inset-block-start:0] [&[data-part=header]>[data-part=close]]:[inset-inline-end:0] [&[data-part=header]>[" +
        "data-part=close]]:grid [&[data-part=header]>[data-part=close]]:place-items-center [&[data-part=header]>[dat" +
        "a-part=close]]:[inline-size:2rem] [&[data-part=header]>[data-part=close]]:[block-size:2rem] [&[data-part=he" +
        "ader]>[data-part=close]]:[min-block-size:0] [&[data-part=header]>[data-part=close]]:p-0 [&[data-part=header" +
        "]>[data-part=close]]:[border-radius:999px] data-[part=body]:flex-auto data-[part=body]:[min-block-size:0] d" +
        "ata-[part=body]:overflow-auto data-[part=body]:overscroll-contain data-[part=footer]:flex data-[part=footer" +
        "]:flex-none data-[part=footer]:flex-wrap data-[part=footer]:justify-end data-[part=footer]:gap-sm [@media_(" +
        "pointer:_coarse)]:data-[part=header]:[padding-inline-end:2.75rem] [@media_(pointer:_coarse)]:[&[data-part=h" +
        "eader]>[data-part=close]]:[min-inline-size:2.75rem] [@media_(pointer:_coarse)]:[&[data-part=header]>[data-p" +
        "art=close]]:[min-block-size:2.75rem] [@media_(pointer:_coarse)]:[&[data-part=footer]>*]:[min-block-size:2.7" +
        "5rem] data-[part=grabber]:hidden data-[part=grabber]:flex-none data-[part=grabber]:[block-size:1.5rem] data" +
        "-[part=grabber]:[margin-block:calc(-1_*_var(--space-md))_calc(-1_*_var(--space-sm))] data-[part=grabber]:[t" +
        "ouch-action:none] data-[part=grabber]:after:content-[''] data-[part=grabber]:after:block data-[part=grabber" +
        "]:after:[inline-size:2.25rem] data-[part=grabber]:after:[block-size:0.25rem] data-[part=grabber]:after:[mar" +
        "gin-block:0.5rem_0] data-[part=grabber]:after:[margin-inline:auto] data-[part=grabber]:after:[border-radius" +
        ":999px] data-[part=grabber]:after:bg-border [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=" +
        "full])]:[inline-size:100%] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[max-inli" +
        "ne-size:100%] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[min-block-size:50dvh]" +
        " [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[max-block-size:80dvh] [@media_(wid" +
        "th_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[margin-block:auto_0] [@media_(width_<_40rem)]:d" +
        "ata-[part=content]:[&:not([data-size=full])]:[margin-inline:0] [@media_(width_<_40rem)]:data-[part=content]" +
        ":[&:not([data-size=full])]:[padding-block-end:calc(var(--space-xl)_+_env(safe-area-inset-bottom))] [@media_" +
        "(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[border-block-end-width:0] [@media_(width_<_" +
        "40rem)]:data-[part=content]:[&:not([data-size=full])]:[border-end-start-radius:0] [@media_(width_<_40rem)]:" +
        "data-[part=content]:[&:not([data-size=full])]:[border-end-end-radius:0] [@media_(width_<_40rem)]:data-[part" +
        "=content]:[&:not([data-size=full]):has(>[data-uiify-modal][data-part=body])]:[touch-action:none] [@media_(w" +
        "idth_<_40rem)]:data-[part=content]:[&:not([data-size=full])_[data-uiify-modal][data-part=header]]:[touch-ac" +
        "tion:none] [@media_(width_<_40rem)]:data-[part=body]:[touch-action:pan-y] [@media_(width_<_40rem)]:data-[pa" +
        "rt=content]:[&:not([data-size=full])>[data-part=grabber]]:block [@media_(width_<_40rem)_and_(prefers-reduce" +
        "d-motion:_no-preference)]:data-[part=content]:[&:not([data-size=full])]:[transition:translate_var(--duratio" +
        "n-fast)_var(--ease-out),_opacity_var(--duration-fast)_var(--ease-out),_display_var(--duration-fast)_allow-d" +
        "iscrete,_overlay_var(--duration-fast)_allow-discrete] [@media_(width_<_40rem)_and_(prefers-reduced-motion:_" +
        "no-preference)]:data-[part=content]:[&:not([data-size=full],[open])]:[translate:0_100%] [@media_(width_<_40" +
        "rem)_and_(prefers-reduced-motion:_no-preference)]:data-[part=content]:[&:not([data-size=full],[open])]:[sca" +
        "le:1] [@media_(width_<_40rem)_and_(prefers-reduced-motion:_no-preference)]:starting:data-[part=content]:[&:" +
        "not([data-size=full])[open]]:[translate:0_100%] [@media_(width_<_40rem)_and_(prefers-reduced-motion:_no-pre" +
        "ference)]:starting:data-[part=content]:[&:not([data-size=full])[open]]:[scale:1] data-[part=content]:data-[" +
        "dragging]:[&[open]:not([data-size=full])]:[transition:none] data-[part=content]:data-[dragging]:[&[open]:no" +
        "t([data-size=full])]:[translate:0_var(--uiify-sheet-offset,_0)] data-[part=content]:data-[backdrop=blur]:ba" +
        "ckdrop:[backdrop-filter:blur(4px)] data-[part=content]:data-[backdrop=none]:backdrop:bg-transparent [&[data" +
        "-part=content]_[data-uiify-modal][data-part=content]::backdrop]:bg-transparent [&[data-part=content]_[data-" +
        "uiify-modal][data-part=content]::backdrop]:[backdrop-filter:none] data-[part=content]:[&:has([data-uiify-mo" +
        "dal][data-part=content]:modal)]:[filter:brightness(0.9)] [@media_(width_>=_40rem)]:data-[part=content]:[&:h" +
        "as([data-uiify-modal][data-part=content]:modal)]:[scale:0.97]"
      }
    >
      <Modal
        id="settings-dialog"
        trigger="Open settings"
        title="Settings"
        description="Update your profile preferences."
      />
    </div>
  );
}
/* app.css */

@layer reset, ui, app, tokens, base, demo, components, utilities, theme;

@import "@gunkin/uiify/styles/reset";

@import "@gunkin/uiify/components/behavior.css";

@import "@gunkin/uiify/styles/tokens";

@import "@gunkin/uiify/styles/tailwind";

@import "tailwindcss/utilities.css";

@theme { --spacing: 0.25rem; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; }

@source inline('[&[data-positioning=native]]:[--uiify-anchor-offset:var(--space-sm)] [&_[data-positioning=native]]:[--uiify-anchor-offset:var(--space-sm)] [&[popover]:popover-open:not([data-positioning=native])]:[margin:revert] [&_[popover]:popover-open:not([data-positioning=native])]:[margin:revert] [&[data-part=content]]:m-auto [&[data-part=content]]:[inline-size:min(28rem,_calc(100vw_-_2_*_var(--space-lg)))] [&[data-part=content]]:p-xl [&[data-part=content]]:border-[length:var(--border-width)] [&[data-part=content]]:border-solid [&[data-part=content]]:border-border [&[data-part=content]]:rounded-xl [&[data-part=content]]:[corner-shape:squircle] [&[data-part=content]]:bg-popover [&[data-part=content]]:text-popover-foreground [&[data-part=content]]:shadow-modal [&[data-part=content]::backdrop]:[background-color:rgb(0_0_0_/_45%)] [&_[data-part=title]]:m-0 [&_[data-part=title]]:text-[length:var(--text-xl)] [&_[data-part=title]]:font-[number:var(--weight-semibold)] [&_[data-part=title]]:[line-height:1.3] [&_[data-part=description]]:[margin-block:var(--space-sm)_var(--space-lg)] [&_[data-part=description]]:text-muted-foreground [&_[data-part=description]]:text-sm [&_[data-part=description]]:[line-height:var(--leading-normal)] motion-safe:[&[data-part=content]]:[transition:var(--transition-presence)] motion-safe:[&[data-part=content]::backdrop]:[transition:var(--transition-presence)] motion-safe:[&[data-part=content]:not([open])]:opacity-0 motion-safe:[&[data-part=content]:not([open])]:[scale:var(--presence-scale)] motion-safe:[&[data-part=content]:not([open])::backdrop]:opacity-0 motion-safe:starting:[&[data-part=content][open]]:opacity-0 motion-safe:starting:[&[data-part=content][open]]:[scale:var(--presence-scale)] motion-safe:starting:[&[data-part=content][open]::backdrop]:opacity-0 data-[part=content]:[--uiify-modal-inline-size:28rem] data-[part=content]:flex-col data-[part=content]:gap-md data-[part=content]:[inline-size:min(var(--uiify-modal-inline-size),_calc(100vw_-_2_*_var(--space-lg)))] data-[part=content]:[max-block-size:calc(100dvh_-_2_*_var(--space-lg))] data-[part=content]:[&[open]]:flex data-[part=content]:data-[size=sm]:[--uiify-modal-inline-size:24rem] data-[part=content]:data-[size=lg]:[--uiify-modal-inline-size:40rem] data-[part=content]:data-[size=full]:inset-0 data-[part=content]:data-[size=full]:m-0 data-[part=content]:data-[size=full]:[inline-size:100%] data-[part=content]:data-[size=full]:[block-size:100%] data-[part=content]:data-[size=full]:[max-inline-size:none] data-[part=content]:data-[size=full]:[max-block-size:none] data-[part=content]:data-[size=full]:[padding-block:max(var(--space-xl),_env(safe-area-inset-top))_max(var(--space-xl),_env(safe-area-inset-bottom))] data-[part=content]:data-[size=full]:[padding-inline:max(var(--space-xl),_env(safe-area-inset-left))_max(var(--space-xl),_env(safe-area-inset-right))] data-[part=content]:data-[size=full]:border-0 data-[part=content]:data-[size=full]:border-current data-[part=content]:data-[size=full]:border-none data-[part=content]:data-[size=full]:rounded-none [@media_(width_>=_40rem)]:data-[part=content]:data-[align=start]:[&:not([data-size=full])]:[margin-block:var(--space-xl)_auto] data-[part=header]:relative data-[part=header]:flex-none data-[part=header]:[padding-inline-end:2.5rem] data-[part=close]:data-[uiify-button]:border-transparent data-[part=close]:data-[uiify-button]:[background-color:color-mix(in_oklch,_var(--foreground)_8%,_transparent)] data-[part=close]:data-[uiify-button]:text-foreground data-[part=close]:data-[uiify-button]:hover:[background-color:color-mix(in_oklch,_var(--foreground)_14%,_transparent)] data-[part=close]:data-[uiify-button]:active:[background-color:color-mix(in_oklch,_var(--foreground)_20%,_transparent)] [&[data-part=header]>[data-part=close]]:absolute [&[data-part=header]>[data-part=close]]:[inset-block-start:0] [&[data-part=header]>[data-part=close]]:[inset-inline-end:0] [&[data-part=header]>[data-part=close]]:grid [&[data-part=header]>[data-part=close]]:place-items-center [&[data-part=header]>[data-part=close]]:[inline-size:2rem] [&[data-part=header]>[data-part=close]]:[block-size:2rem] [&[data-part=header]>[data-part=close]]:[min-block-size:0] [&[data-part=header]>[data-part=close]]:p-0 [&[data-part=header]>[data-part=close]]:[border-radius:999px] data-[part=body]:flex-auto data-[part=body]:[min-block-size:0] data-[part=body]:overflow-auto data-[part=body]:overscroll-contain data-[part=footer]:flex data-[part=footer]:flex-none data-[part=footer]:flex-wrap data-[part=footer]:justify-end data-[part=footer]:gap-sm [@media_(pointer:_coarse)]:data-[part=header]:[padding-inline-end:2.75rem] [@media_(pointer:_coarse)]:[&[data-part=header]>[data-part=close]]:[min-inline-size:2.75rem] [@media_(pointer:_coarse)]:[&[data-part=header]>[data-part=close]]:[min-block-size:2.75rem] [@media_(pointer:_coarse)]:[&[data-part=footer]>*]:[min-block-size:2.75rem] data-[part=grabber]:hidden data-[part=grabber]:flex-none data-[part=grabber]:[block-size:1.5rem] data-[part=grabber]:[margin-block:calc(-1_*_var(--space-md))_calc(-1_*_var(--space-sm))] data-[part=grabber]:[touch-action:none] data-[part=grabber]:after:block data-[part=grabber]:after:[inline-size:2.25rem] data-[part=grabber]:after:[block-size:0.25rem] data-[part=grabber]:after:[margin-block:0.5rem_0] data-[part=grabber]:after:[margin-inline:auto] data-[part=grabber]:after:[border-radius:999px] data-[part=grabber]:after:bg-border [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[inline-size:100%] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[max-inline-size:100%] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[min-block-size:50dvh] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[max-block-size:80dvh] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[margin-block:auto_0] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[margin-inline:0] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[padding-block-end:calc(var(--space-xl)_+_env(safe-area-inset-bottom))] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[border-block-end-width:0] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[border-end-start-radius:0] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])]:[border-end-end-radius:0] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full]):has(>[data-uiify-modal][data-part=body])]:[touch-action:none] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])_[data-uiify-modal][data-part=header]]:[touch-action:none] [@media_(width_<_40rem)]:data-[part=body]:[touch-action:pan-y] [@media_(width_<_40rem)]:data-[part=content]:[&:not([data-size=full])>[data-part=grabber]]:block [@media_(width_<_40rem)_and_(prefers-reduced-motion:_no-preference)]:data-[part=content]:[&:not([data-size=full])]:[transition:translate_var(--duration-fast)_var(--ease-out),_opacity_var(--duration-fast)_var(--ease-out),_display_var(--duration-fast)_allow-discrete,_overlay_var(--duration-fast)_allow-discrete] [@media_(width_<_40rem)_and_(prefers-reduced-motion:_no-preference)]:data-[part=content]:[&:not([data-size=full],[open])]:[translate:0_100%] [@media_(width_<_40rem)_and_(prefers-reduced-motion:_no-preference)]:data-[part=content]:[&:not([data-size=full],[open])]:[scale:1] [@media_(width_<_40rem)_and_(prefers-reduced-motion:_no-preference)]:starting:data-[part=content]:[&:not([data-size=full])[open]]:[translate:0_100%] [@media_(width_<_40rem)_and_(prefers-reduced-motion:_no-preference)]:starting:data-[part=content]:[&:not([data-size=full])[open]]:[scale:1] data-[part=content]:data-[dragging]:[&[open]:not([data-size=full])]:[transition:none] data-[part=content]:data-[dragging]:[&[open]:not([data-size=full])]:[translate:0_var(--uiify-sheet-offset,_0)] data-[part=content]:data-[backdrop=blur]:backdrop:[backdrop-filter:blur(4px)] data-[part=content]:data-[backdrop=none]:backdrop:bg-transparent [&[data-part=content]_[data-uiify-modal][data-part=content]::backdrop]:bg-transparent [&[data-part=content]_[data-uiify-modal][data-part=content]::backdrop]:[backdrop-filter:none] data-[part=content]:[&:has([data-uiify-modal][data-part=content]:modal)]:[filter:brightness(0.9)] [@media_(width_>=_40rem)]:data-[part=content]:[&:has([data-uiify-modal][data-part=content]:modal)]:[scale:0.97]');

@source inline("data-[part=grabber]:after:content-['']");

@layer base { :root { --uiify-control-text: var(--text-sm); } @media (pointer: coarse) { :root { --uiify-control-text: var(--text-base); } } }

/* Authored animation and registered properties used by the Tailwind skin.
 * Visual rules belong in utility recipes; only at-rule definitions live here. */
@layer components {
  @keyframes uiify-presence-in {
    from {
      opacity: 0;
      scale: var(--presence-scale);
    }
  }
}

@layer components {
  @keyframes uiify-presence-out {
    to {
      opacity: 0;
      scale: var(--presence-scale);
    }
  }
}

@property --uiify-spinner-tail {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --uiify-spinner-length {
  syntax: "<angle>";
  inherits: false;
  initial-value: 36deg;
}

@layer components {
  @keyframes uiify-spinner-rotate {
    to {
      transform: rotate(360deg);
    }
  }
}

@layer components {
  @keyframes uiify-spinner-orbit {
    0% {
      --uiify-spinner-tail: 0deg;
      --uiify-spinner-length: 36deg;
    }

    50% {
      --uiify-spinner-tail: 90deg;
      --uiify-spinner-length: 324deg;
    }

    100% {
      --uiify-spinner-tail: 360deg;
      --uiify-spinner-length: 36deg;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-pulse {
    50% {
      opacity: 0.45;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-shimmer {
    to {
      background-position: -200% 0;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-reveal {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
}

@property --candle-body-height {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-body-top {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --candle-slot {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 100%;
}

@property --candle-wick-height {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-wick-top {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --candle-x {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

@property --line-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-prev-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-count {
  syntax: "<integer>";
  inherits: false;
  initial-value: 1;
}

@property --line-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

@layer components {
  @keyframes uiify-otp-input-caret-blink {
    50% {
      opacity: 0;
    }
  }
}

Exemplos

Sizes

size writes data-size: sm 24rem, md (default) 28rem, lg 40rem, full fills the viewport. On a phone every size except full is a bottom sheet.

Small

24rem wide.

Large

40rem wide.

import { Modal } from "@gunkin/uiify";

export default function Example() {
  return (
    <>
      <Modal id="size-sm" size="sm" title="Small" trigger="Small">
        <p>24rem wide.</p>
      </Modal>
      <Modal id="size-lg" size="lg" title="Large" trigger="Large">
        <p>40rem wide.</p>
      </Modal>
    </>
  );
}

Long content

The title, content and footer share one scroll area. Compose the parts inside ModalBody for a document-style dialog.

Terms of service

The whole document scrolls together.

Clause 1: text that makes the document taller than the screen.

Clause 2: text that makes the document taller than the screen.

Clause 3: text that makes the document taller than the screen.

Clause 4: text that makes the document taller than the screen.

Clause 5: text that makes the document taller than the screen.

Clause 6: text that makes the document taller than the screen.

Clause 7: text that makes the document taller than the screen.

Clause 8: text that makes the document taller than the screen.

Clause 9: text that makes the document taller than the screen.

Clause 10: text that makes the document taller than the screen.

Clause 11: text that makes the document taller than the screen.

Clause 12: text that makes the document taller than the screen.

Clause 13: text that makes the document taller than the screen.

Clause 14: text that makes the document taller than the screen.

Clause 15: text that makes the document taller than the screen.

Clause 16: text that makes the document taller than the screen.

Clause 17: text that makes the document taller than the screen.

Clause 18: text that makes the document taller than the screen.

Clause 19: text that makes the document taller than the screen.

Clause 20: text that makes the document taller than the screen.

Clause 21: text that makes the document taller than the screen.

Clause 22: text that makes the document taller than the screen.

Clause 23: text that makes the document taller than the screen.

Clause 24: text that makes the document taller than the screen.

Clause 25: text that makes the document taller than the screen.

Clause 26: text that makes the document taller than the screen.

Clause 27: text that makes the document taller than the screen.

Clause 28: text that makes the document taller than the screen.

Clause 29: text that makes the document taller than the screen.

Clause 30: text that makes the document taller than the screen.

import { ModalTrigger, ModalContent, ModalBody, ModalHeader, ModalTitle, ModalClose, ModalFooter } from "@gunkin/uiify";
export default function Example() {
  return <>
    <ModalTrigger target="terms">Read terms</ModalTrigger>
    <ModalContent id="terms" aria-labelledby="terms-title">
      <ModalBody>
        <ModalHeader><ModalTitle id="terms-title">Terms of service</ModalTitle><ModalClose aria-label="Close" target="terms">×</ModalClose></ModalHeader>
        {Array.from({ length: 30 }, (_, index) => <p key={index}>Clause {index + 1}: text that makes the document taller than the screen.</p>)}
        <ModalFooter><ModalClose target="terms">Done</ModalClose></ModalFooter>
      </ModalBody>
    </ModalContent>
  </>;
}

Opening a modal from a modal

Render the child Modal inside the parent. It opens on top, Escape closes only the child, focus returns to the button that opened it, and the page is dimmed once.

Edit profile

Unsaved changes are lost when you close.

Discard changes?

This cannot be undone.

import { Modal, ModalClose } from "@gunkin/uiify";

export default function Example() {
  return (
    <Modal
      id="editor"
      trigger="Edit profile"
      title="Edit profile"
      description="Unsaved changes are lost when you close."
    >
      <Modal
        id="discard"
        trigger="Discard changes"
        title="Discard changes?"
        description="This cannot be undone."
        footer={<ModalClose target="discard">Keep editing</ModalClose>}
      />
    </Modal>
  );
}

API

Modal

ModalTrigger

PropTipoPadrãoDescrição
target*string—Id do ModalContent aberto por este botão; o navegador o resolve no clique, sem executar JavaScript nosso.
childrenReactNode—Conteúdo do botão acionador.
asTAs & NativeButtonAs<TAs>"button"Componente que encaminha props e ref a um botão nativo; outras tags são rejeitadas, pois comandos de invocador exigem um botão real.

ModalContent

PropTipoPadrãoDescrição
id*string—Id único fornecido pelo consumidor; referenciado pelo acionador e pelo destino de fechamento.
size"sm" | "md" | "lg" | "full""md"Faixa de largura (24, 28, 40rem). full preenche a área visível. Gravada em data-size; substitua --uiify-modal-inline-size para uma largura personalizada.
align"center" | "start""center"Posição vertical em tablets e computadores. Celulares recebem um painel inferior, salvo quando size é full.
dismiss"any" | "closerequest" | "none""any"closedby: any acrescenta toques no fundo, closerequest permite apenas Escape e none permite apenas seus controles. Safari não oferece closedby, então Escape sempre fecha; UIEnhance trata toques no fundo (any) e gestos no painel (exceto none).
backdrop"opaque" | "blur" | "none""opaque"Tratamento do fundo. Gravado em data-backdrop.
childrenReactNode—Conteúdo do diálogo.

ModalClose

PropTipoPadrãoDescrição
target*string—Id do ModalContent fechado por este botão.
childrenReactNode—Conteúdo do botão de fechar.
asTAs & NativeButtonAs<TAs>"button"Componente que encaminha props e ref a um botão nativo; outras tags são rejeitadas, pois comandos de invocador exigem um botão real.

ModalHeader

PropTipoPadrãoDescrição
childrenReactNode—Linha superior fixa com título, descrição e controle de fechamento.

ModalTitle

PropTipoPadrãoDescrição
childrenReactNode—O nome acessível do diálogo; referencie seu id em aria-labelledby.

ModalDescription

PropTipoPadrãoDescrição
childrenReactNode—Texto de apoio; referencie seu id em aria-describedby.

ModalBody

PropTipoPadrãoDescrição
childrenReactNode—A região de rolagem entre cabeçalho e rodapé.

ModalFooter

PropTipoPadrãoDescrição
childrenReactNode—Linha de ações fixada abaixo do corpo.

ModalGrabber

PropTipoPadrãoDescrição
childrenReactNode—Alça de arraste decorativa no painel móvel; o gesto começa aqui.

ModalClientContent (@gunkin/uiify/components/modal/client)

PropTipoPadrãoDescrição
id*string—Id único fornecido pelo consumidor; referenciado pelo acionador e pelo controle de fechamento.
openboolean—Estado de abertura controlado, que prevalece após a hidratação.
defaultOpenboolean—Estado inicial não controlado, aplicado com showModal() após a hidratação; a marcação do servidor permanece fechada.
onOpenChange(open: boolean) => void—Dispara uma vez por alteração nativa ou programática com o novo estado, inclusive uma vez na montagem se o navegador tiver aberto o diálogo antes da hidratação.
onOpenChangeComplete(open: boolean) => void—Dispara uma vez por transição de abertura ou fechamento após o fim das transições CSS, ou imediatamente quando não há transições, incluindo a transição de defaultOpen após a hidratação; nunca quando o diálogo não muda.
size"sm" | "md" | "lg" | "full""md"Faixa de largura (24, 28, 40rem). full preenche a área visível. Gravada em data-size; substitua --uiify-modal-inline-size para uma largura personalizada.
align"center" | "start""center"Posição vertical em tablets e computadores. Celulares recebem um painel inferior, salvo quando size é full.
dismiss"any" | "closerequest" | "none""any"closedby: any acrescenta toques no fundo, closerequest permite apenas Escape e none permite apenas seus controles. Safari não oferece closedby, então Escape sempre fecha; UIEnhance trata toques no fundo (any) e gestos no painel (exceto none).
backdrop"opaque" | "blur" | "none""opaque"Tratamento do fundo. Gravado em data-backdrop.
childrenReactNode—Conteúdo do diálogo.

Teclado

TeclaAção
EscapeFeche (apenas o modal superior quando houver aninhamento)
TabPercorra o foco dentro do modal
Shift+TabInverta a ordem de tabulação

Acessibilidade

<dialog> nativo aberto por um invocador command="show-modal" antes de qualquer JavaScript. O navegador controla modalidade, contenção de foco, Escape e retorno de foco; o fechamento usa request-close para permitir veto por onCancel. O ícone de fechar no cabeçalho está sempre presente. Em celulares, o modal é um painel inferior; deslizar para baixo e tocar no fundo são atalhos, nunca as únicas formas de sair.

Pontos de integração de estilos

data-uiify-modaldata-part="trigger|content|header|title|description|body|footer|grabber|close"data-size="sm|lg|full"data-align="start"data-backdrop="blur|none"--uiify-trigger-open (inside [data-uiify-modal][data-part="root"])--uiify-modal-inline-size (dialog width)
Esta página foi traduzida automaticamente e ainda não foi revisada.