Ir al contenido

Alert modal

Diálogo que pide al usuario confirmar o cancelar una acción.

Navegadores
Chrome 123+Firefox 121+Safari 17.5+
Nivel
0
Tamaño comprimido
555 B
Presupuesto de diseño forzado
0 ms
Presupuesto de axe
0
Compatible con SSR
Sí
Basado en
elements/dialog
Contrato publicado
Entorno de pruebas

Zona de pruebas

Alert Modal

Closed

Delete item?

Esta acción no se puede deshacer.

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

export default function Example() {
  return (
    <div className="">
      <AlertModal
        id="delete-dialog"
        trigger="Delete item"
        title="Delete item?"
        description="This action cannot be undone."
      />
    </div>
  );
}
/* Button — pill-shaped and monochrome: a filled foreground action by default, secondary for
   the tonal one, outline for the bordered one, ghost for a surface that only appears on
   hover, link for inline text actions.

   Filled variants are lit from above by two layers that sit between the fill and the label:
   - `::before`, the wash: a blurred copy of the shape raised by half its height, so only its
     soft lower edge glows inside the upper half. `filter` runs before `clip-path`, which lets
     one box blur itself and still be cut back to the button outline.
   - `::after`, the rim: a half-pixel ring whose colour fades out towards the bottom. A
     box-shadow cannot take a gradient and `border-image` ignores the radius, so the ring is
     a gradient box with its content box masked out. */

@layer components {
  :where([data-uiify-button]) {
    --uiify-button-rim: rgb(255 255 255 / 20%);
    --uiify-button-wash: light-dark(rgb(255 255 255 / 15%), rgb(255 255 255 / 30%));

    position: relative;
    isolation: isolate;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    min-block-size: 44px;

    /* Never narrower than tall: an icon size only has to drop the padding to be a circle. */
    min-inline-size: 44px;
    padding-block: 0;
    padding-inline: 1.25rem;
    border: var(--border-width) solid transparent;
    border-radius: var(--radius-full);
    background-color: var(--foreground);
    background-clip: padding-box;
    color: var(--background);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    line-height: 1.5rem;
    text-decoration: none;
    cursor: pointer;
    transition: var(--transition-colors);
  }

  :where([data-uiify-button])::before,
  :where([data-uiify-button])::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    pointer-events: none;
  }

  :where([data-uiify-button])::before {
    background-color: var(--uiify-button-wash);
    filter: blur(10px);
    translate: 0 -50%;
    clip-path: inset(50% 0 -50% 0 round var(--radius-full));
  }

  :where([data-uiify-button])::after {
    padding: 0.5px;
    background-image: linear-gradient(to bottom, var(--uiify-button-rim), transparent 75%);
    mask:
      linear-gradient(#fff 0 0) content-box exclude,
      linear-gradient(#fff 0 0);
  }

  /* Sizes: md is the default metrics above. */
  [data-uiify-button]:is([data-size="sm"], [data-size="icon-sm"]) {
    min-block-size: 32px;
    min-inline-size: 32px;
    padding-inline: 0.75rem;
    gap: 0.25rem;
    font-size: var(--text-sm);
    line-height: 1.25rem;
  }

  [data-uiify-button]:is([data-size="lg"], [data-size="icon-lg"]) {
    min-block-size: 56px;
    min-inline-size: 56px;
    padding-inline: 1.75rem;
    font-size: var(--text-lg);
    line-height: 1.75rem;
  }

  /* Icon sizes: the same three heights with no padding, for a single icon. */
  [data-uiify-button][data-size^="icon"] {
    padding-inline: 0;
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--foreground) 90%, var(--background));
    }
  }

  [data-uiify-button]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 80%, var(--background));
  }

  [data-uiify-button]:disabled,
  [data-uiify-button][data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* Secondary: tonal low-emphasis fill. */
  [data-uiify-button][data-variant="secondary"] {
    --uiify-button-rim: light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 10%));
    --uiify-button-wash: light-dark(rgb(255 255 255 / 20%), rgb(255 255 255 / 5%));

    background-color: color-mix(in oklch, var(--foreground) 10%, transparent);
    color: var(--foreground);
  }

  /* Outline: no fill and no wash, a hairline border in the text colour. Its rim is a full
     border-width ring laid over that border, so the top edge reads slightly stronger and
     fades into the plain border below. */
  [data-uiify-button][data-variant="outline"] {
    --uiify-button-rim: color-mix(in oklch, var(--foreground) 25%, transparent);

    border-color: color-mix(in oklch, var(--foreground) 20%, transparent);
    background-color: transparent;
    color: var(--foreground);
  }

  [data-uiify-button][data-variant="outline"]::before {
    content: none;
  }

  [data-uiify-button][data-variant="outline"]::after {
    inset: calc(-1 * var(--border-width));
    padding: var(--border-width);
  }

  [data-uiify-button][data-variant="outline"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 16%, transparent);
  }

  /* Ghost: text only; the tonal surface appears on hover. Flat, so no light layers. */
  [data-uiify-button][data-variant="ghost"] {
    background-color: transparent;
    color: var(--foreground);
  }

  [data-uiify-button][data-variant="ghost"]::before,
  [data-uiify-button][data-variant="ghost"]::after {
    content: none;
  }

  [data-uiify-button][data-variant="ghost"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 16%, transparent);
  }

  [data-uiify-button][data-variant="secondary"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 26%, transparent);
  }

  /* Destructive: irreversible or dangerous actions. */
  [data-uiify-button][data-variant="destructive"] {
    --uiify-button-wash: rgb(255 255 255 / 15%);

    background-color: var(--destructive);
    color: var(--destructive-foreground);
  }

  [data-uiify-button][data-variant="destructive"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--destructive), black 20%);
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button][data-variant="outline"]:hover:not(:disabled, [data-disabled]),
    [data-uiify-button][data-variant="ghost"]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--foreground) 10%, transparent);
    }

    [data-uiify-button][data-variant="secondary"]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--foreground) 20%, transparent);
    }

    [data-uiify-button][data-variant="destructive"]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--destructive), black 12%);
    }
  }
}

@layer components {
  /* Selected: the chosen item of a set or a toggle that is on, filled with the brand colour.
     The state is read from whatever already carries it: the `selected` prop, a pressed toggle,
     the current page, or — for a label styled as a button — the checked native control right
     before it, so a toggle works with no JavaScript. */
  [data-uiify-button]:is([data-selected], [aria-pressed="true"], [aria-current="page"]),
  :checked + [data-uiify-button] {
    --uiify-button-rim: rgb(255 255 255 / 20%);
    --uiify-button-wash: rgb(255 255 255 / 15%);

    border-color: transparent;
    background-color: var(--primary);
    color: var(--primary-foreground);
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button]:is([data-selected], [aria-pressed="true"], [aria-current="page"]):hover:not(
        :disabled,
        [data-disabled]
      ),
    :checked:not(:disabled) + [data-uiify-button]:hover {
      background-color: color-mix(in oklch, var(--primary) 88%, var(--primary-foreground));
    }
  }

  /* A label styled as a button follows the native control it belongs to. */
  :disabled + [data-uiify-button] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  [data-uiify-button][hidden] {
    display: none;
  }
}

@layer components {
  /* Link: an inline text action with no surface, on a button or an anchor alike. */
  [data-uiify-button][data-variant="link"],
  [data-uiify-button][data-variant="link"]:active {
    padding-inline: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: var(--primary);
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }
  [data-uiify-button][data-variant="link"] {
    min-inline-size: 0;
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button][data-variant="link"]:hover {
      padding-inline: 0;
      border: 0;
      border-radius: 0;
      background: transparent;
      color: var(--primary);
      text-decoration: underline;
      text-underline-offset: 0.2em;
    }
  }

  [data-uiify-button][data-variant="link"]::before,
  [data-uiify-button][data-variant="link"]::after {
    content: none;
  }

  [data-uiify-button][data-variant="link"]:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 3px;
  }
}

/* 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 { AlertModal } 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
        [&[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]:gap-md
        data-[part=content]:hidden data-[part=content]:[&[open]]:grid [&_[data-part=footer]]:flex
        [&_[data-part=footer]]:justify-end [&_[data-part=footer]]:gap-sm [&_[data-part=footer]]:pt-md
        [&_[data-part=footer]]:[border-block-start-width:1px] [&_[data-part=footer]]:[border-block-start-style:solid]
        [&_[data-part=footer]]:[border-block-start-color:var(--border)]
      "
    >
      <AlertModal
        id="delete-dialog"
        trigger="Delete item"
        title="Delete item?"
        description="This action cannot be undone."
      />
    </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]:gap-md data-[part=content]:hidden data-[part=content]:[&[open]]:grid [&_[data-part=footer]]:flex [&_[data-part=footer]]:justify-end [&_[data-part=footer]]:gap-sm [&_[data-part=footer]]:pt-md [&_[data-part=footer]]:[border-block-start-width:1px] [&_[data-part=footer]]:[border-block-start-style:solid] [&_[data-part=footer]]:[border-block-start-color:var(--border)]');

@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;
    }
  }
}

API

AlertModal

PropTipoPredeterminadoDescripción
id*string—Id único del elemento alertdialog al que apuntan el activador y los botones de cancelación y acción.
trigger*ReactNode—Contenido del botón que abre el diálogo.
title*ReactNode—Encabezado renderizado dentro del diálogo que se usa como nombre accesible.
descriptionReactNode—Texto de apoyo renderizado bajo el título y referenciado por aria-describedby.
childrenReactNode—Contenido del cuerpo situado entre la descripción y los dos botones.
actionLabelReactNode"Confirm"Contenido del botón de confirmación, que cierra con el valor "confirm".
cancelLabelReactNode"Cancel"Contenido del botón de cancelación, que cierra con el valor "cancel".
classNamestring—Clase aplicada al elemento dialog.

AlertModalTrigger

PropTipoPredeterminadoDescripción
target*string—Id del AlertModalContent que abre este botón; el navegador lo resuelve al hacer clic.
childrenReactNode—Contenido del botón activador.
asTAs & NativeButtonAs<TAs>"button"Componente que pasa props y ref a un botón nativo; se rechazan otras etiquetas porque los comandos invoker necesitan un botón real.

AlertModalContent

PropTipoPredeterminadoDescripción
id*string—Id único proporcionado por el consumidor, referenciado por las demás partes.
childrenReactNode—Contenido del diálogo, que se renderiza con role="alertdialog".

AlertModalAction

PropTipoPredeterminadoDescripción
target*string—Id del AlertModalContent que cierra este botón para confirmar.
childrenReactNode—Contenido del botón de confirmación.
asTAs & NativeButtonAs<TAs>"button"Componente que pasa props y ref a un botón nativo; se rechazan otras etiquetas porque los comandos invoker necesitan un botón real.

AlertModalCancel

PropTipoPredeterminadoDescripción
target*string—Id del AlertModalContent que cierra este botón para cancelar.
childrenReactNode—Contenido del botón de cancelación.
asTAs & NativeButtonAs<TAs>"button"Componente que pasa props y ref a un botón nativo; se rechazan otras etiquetas porque los comandos invoker necesitan un botón real.

Teclado

TeclaAcción
TabLlevar el foco al control o sacarlo de él
Enter / SpaceActivar cuando tiene el foco (controles de tipo botón)

Accesibilidad

Usa HTML semántico siempre que es posible. Inspecciona el marcado renderizado en el entorno de pruebas: ARIA se añade solo cuando los elementos nativos no bastan.

Puntos de estilo

data-disabled