orn-ui
Organism

BottomSheet

Requiere <UIProvider> — Primeros pasos →

Instalación

Instala solo este componente (copia el código fuente a tu proyecto, sin dependencia npm)

npx orn-ui add bottom-sheet

O instala el paquete completo e impórtalo

pnpm add orn-ui
import { BottomSheet } from 'orn-ui/bottom-sheet';
Depende de:core (theme + icons)Typographyicon-buttonpressable-scaletransition

Uso

  • cuándo usarlo — Acciones o contenido secundario sobre la pantalla actual, sin salir de ella.
  • visible / onClose — Controlado: cierra con el fondo y con el gesto, siempre a través de `onClose`.
  • title / footer — Con título aparece el header, con su botón de cerrar. `footerPlacement` decide dónde vive el footer: fijo, al final del scroll, u oculto mientras el teclado está abierto.
  • draggable / maxHeight — `draggable={false}` para una hoja que sólo cierra por botón o fondo. `maxHeight` como fracción 0-1 de la pantalla, o en píxeles.
Clip del demo todavía sin grabar — ver MEDIA.md

Variantes

bare — handle only

<>
  <Button title="Open sheet" onPress={() => setBare(true)} />
  <BottomSheet visible={bare} onClose={() => setBare(false)}>
    <Title style={{ marginBottom: 8 }}>Bottom sheet</Title>
    <Body>Drag the handle down, or tap outside, to dismiss.</Body>
  </BottomSheet>
</>

title + footer — a form in a sheet

<>
  <Button title="Open form" onPress={() => setForm(true)} />
  <BottomSheet
    visible={form}
    onClose={() => setForm(false)}
    title="Add a note"
    footer={
      <FormActions
        primaryLabel="Save"
        onPrimaryPress={() => setForm(false)}
        secondaryLabel="Cancel"
        onSecondaryPress={() => setForm(false)}
      />
    }
  >
    <Input label="Note" placeholder="Anything worth remembering" value={note} onChangeText={setNote} />
  </BottomSheet>
</>

long content — scrolls up to maxHeight

<>
  <Button title="Open long sheet" onPress={() => setLong(true)} />
  <BottomSheet visible={long} onClose={() => setLong(false)} title="Twelve rows">
    <View style={{ gap: 12 }}>
      {LONG.map((row) => (
        <Body key={row}>{row}</Body>
      ))}
    </View>
  </BottomSheet>
</>

footerPlacement="hide-with-keyboard" — the form keeps that row

<>
  <Button title="Open long form" onPress={() => setHiding(true)} />
  <BottomSheet
    visible={hiding}
    onClose={() => setHiding(false)}
    title="Long form"
    footerPlacement="hide-with-keyboard"
    footer={
      <FormActions
        primaryLabel="Save"
        onPrimaryPress={() => setHiding(false)}
        secondaryLabel="Cancel"
        onSecondaryPress={() => setHiding(false)}
      />
    }
  >
    <View style={{ gap: 12 }}>
      {['Name', 'Company', 'Email', 'Phone', 'Address'].map((label) => (
        <Input key={label} label={label} value={note} onChangeText={setNote} />
      ))}
    </View>
  </BottomSheet>
</>

draggable={false} + maxHeight={0.4}

<>
  <Button title="Open fixed sheet" onPress={() => setFixed(true)} />
  <BottomSheet
    visible={fixed}
    onClose={() => setFixed(false)}
    title="No dragging"
    draggable={false}
    maxHeight={0.4}
  >
    <Body>Dismiss with the close button or by tapping outside.</Body>
  </BottomSheet>
</>
Código completo del demo
const variants: VariantDef[] = [
  {
    label: 'bare — handle only',
    content: (
      <>
        <Button title="Open sheet" onPress={() => setBare(true)} />
        <BottomSheet visible={bare} onClose={() => setBare(false)}>
          <Title style={{ marginBottom: 8 }}>Bottom sheet</Title>
          <Body>Drag the handle down, or tap outside, to dismiss.</Body>
        </BottomSheet>
      </>
    ),
  },
  {
    label: 'title + footer — a form in a sheet',
    content: (
      <>
        <Button title="Open form" onPress={() => setForm(true)} />
        <BottomSheet
          visible={form}
          onClose={() => setForm(false)}
          title="Add a note"
          footer={
            <FormActions
              primaryLabel="Save"
              onPrimaryPress={() => setForm(false)}
              secondaryLabel="Cancel"
              onSecondaryPress={() => setForm(false)}
            />
          }
        >
          <Input label="Note" placeholder="Anything worth remembering" value={note} onChangeText={setNote} />
        </BottomSheet>
      </>
    ),
  },
  {
    label: 'long content — scrolls up to maxHeight',
    content: (
      <>
        <Button title="Open long sheet" onPress={() => setLong(true)} />
        <BottomSheet visible={long} onClose={() => setLong(false)} title="Twelve rows">
          <View style={{ gap: 12 }}>
            {LONG.map((row) => (
              <Body key={row}>{row}</Body>
            ))}
          </View>
        </BottomSheet>
      </>
    ),
  },
  {
    label: 'footerPlacement="hide-with-keyboard" — the form keeps that row',
    content: (
      <>
        <Button title="Open long form" onPress={() => setHiding(true)} />
        <BottomSheet
          visible={hiding}
          onClose={() => setHiding(false)}
          title="Long form"
          footerPlacement="hide-with-keyboard"
          footer={
            <FormActions
              primaryLabel="Save"
              onPrimaryPress={() => setHiding(false)}
              secondaryLabel="Cancel"
              onSecondaryPress={() => setHiding(false)}
            />
          }
        >
          <View style={{ gap: 12 }}>
            {['Name', 'Company', 'Email', 'Phone', 'Address'].map((label) => (
              <Input key={label} label={label} value={note} onChangeText={setNote} />
            ))}
          </View>
        </BottomSheet>
      </>
    ),
  },
  {
    label: 'draggable={false} + maxHeight={0.4}',
    content: (
      <>
        <Button title="Open fixed sheet" onPress={() => setFixed(true)} />
        <BottomSheet
          visible={fixed}
          onClose={() => setFixed(false)}
          title="No dragging"
          draggable={false}
          maxHeight={0.4}
        >
          <Body>Dismiss with the close button or by tapping outside.</Body>
        </BottomSheet>
      </>
    ),
  },
];
return <VariantList variants={variants} />;

Props

Además acepta testID, que se reenvía al nodo raíz. Existe para los tests end-to-end (Maestro maneja la app por el árbol de accesibilidad) y no cambia en nada cómo se ve ni cómo se comporta el componente.

NombreTipoPor defectoDescripción
visibleboolean
onClose() => void
title?stringCon título aparece el header, con su botón de cerrar.
footer?ReactNodeAcciones de la hoja. Dónde viven lo decide `footerPlacement`.
footerPlacement?'fixed' | 'scroll' | 'hide-with-keyboard'fixed'fixed' lo ancla al fondo de la hoja; 'scroll' lo deja al final del contenido; 'hide-with-keyboard' lo esconde mientras se tipea, que en un formulario largo le devuelve esa franja al campo enfocado.
scrollable?booleantrueEl contenido scrollea cuando no entra.
draggable?booleantrueArrastrar la hoja hacia abajo la cierra.
maxHeight?number0.9Fracción de la pantalla (0-1) o píxeles.
closeAccessibilityLabel?stringClose
containerStyle?ViewStyle
contentStyle?ViewStyle