Overlays

ShareSheet

A bottom sheet with the ways to share a link: copy it, email, X, LinkedIn, WhatsApp and more, and the system's own.

#social#dialog#overlay#mobile#interactive

Demo

Preview
Not shared yet.

Invite to the project

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/share_sheet.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/share_sheet.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/share_sheet.qwe
component ShareSheetDemoLive() {    state sharing = false    state how = null    <div.demo-row>        <Button variant="soft" on_click={() => { sharing = true }}>Invite (from state)</Button>        <span.demo-note>{how == null ? "Not shared yet." : `Shared by ${how}.`}</span>        <ShareSheet title="Invite to the project" url="https://quillolang.org/join/7f3a" text="Join my project on Quillo"                    targets={["copy", "email", "whatsapp", "telegram"]} open={sharing}                    on_share={(t) => { how = t }} on_close={() => { sharing = false }}/>    </div>}view ShareSheetDemo() =    <div.demo-row>        <ShareSheet url="https://quillolang.org/components/" text="Quillo UI: components for Quillo" trigger="Share"/>        <ShareSheetDemoLive/>    </div>

Source

ui/share_sheet.qwe
// ShareSheet: a panel rising from the bottom of the screen with the ways to share a// link: copy it (it says "Link copied"), email, X, LinkedIn, Facebook, WhatsApp,// Reddit, Telegram, and the system's own share sheet where the browser has one// ("More…"). The link is shown in full above them. It is the browser's own <dialog>,// so focus stays in it, and Escape or a click above it closes it.////   <ShareSheet url="https://example.com/posts/7" text="A post worth reading" trigger="Share"/>//   <ShareSheet url={link} targets={["copy", "email", "whatsapp"]} open={sharing}//               on_share={(how) => { track(how) }} on_close={() => { sharing = false }}/>//// url: the link (this page's address by default). text: what goes with it (the// subject of an email, the words of a post; the page's title by default).// targets: which, in order, of "copy", "email", "x", "linkedin", "facebook",// "whatsapp", "reddit", "telegram" and "more" (all of them by default; "more" shows// only where the browser has navigator.share). title: the sheet's heading ("Share").// trigger: the text of a button that opens it (styled as a Button when button_css is// on the page). Inside one of your components, open it from your own state instead:// open={...} and on_close(). on_share(target): called with the target used.import ui/theme.qwe use ui_clslet share_sheet_css = css`.ui-share-sheet-host { display: contents; }.ui-share-sheet {  box-sizing: border-box; inset: auto 0 0 0; width: min(560px, 100vw); max-width: 100vw; max-height: calc(100dvh - 24px); margin: 0 auto;  padding: 0; overflow: auto; border: 1px solid var(--ui-border); border-bottom: 0;  border-radius: calc(var(--ui-radius) + 6px) calc(var(--ui-radius) + 6px) 0 0; background: var(--ui-surface); color: var(--ui-fg);  box-shadow: var(--ui-shadow-lg); font: 14px/1.45 var(--ui-font);}.ui-share-sheet[open] { animation: ui-share-sheet-up .24s cubic-bezier(.2, .8, .2, 1); }.ui-share-sheet::backdrop { background: var(--ui-overlay); }@keyframes ui-share-sheet-up { from { transform: translateY(100%); } }.ui-share-sheet-box { padding: 10px 20px max(20px, env(safe-area-inset-bottom)); }.ui-share-sheet-grip { width: 40px; height: 5px; margin: 0 auto 12px; border-radius: 999px; background: var(--ui-strong); }.ui-share-sheet-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }.ui-share-sheet-head h2 { margin: 0; font-size: 17px; font-weight: 600; }.ui-share-sheet-x {  display: grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 0; border-radius: var(--ui-radius-sm);  background: transparent; color: var(--ui-muted); cursor: pointer;}.ui-share-sheet-x:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-share-sheet-x svg { width: 18px; height: 18px; }.ui-share-sheet-link { display: flex; gap: 8px; margin-bottom: 16px; }.ui-share-sheet-url {  flex: 1; min-width: 0; height: 36px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);  background: var(--ui-subtle); color: var(--ui-fg); font: 13px/1 var(--ui-mono);}.ui-share-sheet-url:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-share-sheet-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-share-sheet-target {  display: grid; justify-items: center; gap: 6px; width: 100%; padding: 10px 4px; border: 0; border-radius: var(--ui-radius-sm);  background: transparent; color: var(--ui-fg); font: 12px/1.3 var(--ui-font); text-align: center; text-decoration: none; cursor: pointer;}.ui-share-sheet-target:hover { background: var(--ui-subtle); }.ui-share-sheet-icon {  display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%; background: var(--ui-accent-soft); color: var(--ui-accent);  font: 700 16px/1 var(--ui-font);}.ui-share-sheet-icon svg { width: 20px; height: 20px; }.ui-share-sheet-ok .ui-share-sheet-icon { background: var(--ui-success-soft); color: var(--ui-success); }.ui-share-sheet-x:focus-visible, .ui-share-sheet-target:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (prefers-reduced-motion: reduce) { .ui-share-sheet[open] { animation: none; } }`// Where each target goes with the link and its text; null for those that are not links.fn share_sheet_href(target: str, url: str, text: str) =    match target {        "email" => `mailto:?${url_encode({ subject: text, body: url })}`        "x" => `https://x.com/intent/post?${url_encode({ text, url })}`        "linkedin" => `https://www.linkedin.com/sharing/share-offsite/?${url_encode({ url })}`        "facebook" => `https://www.facebook.com/sharer/sharer.php?${url_encode({ u: url })}`        "whatsapp" => `https://wa.me/?${url_encode({ text: `${text} ${url}` })}`        "reddit" => `https://www.reddit.com/submit?${url_encode({ url, title: text })}`        "telegram" => `https://t.me/share/url?${url_encode({ url, text })}`        _ => null    }fn share_sheet_name(target: str) =    match target {        "copy" => "Copy link"        "email" => "Email"        "x" => "X"        "linkedin" => "LinkedIn"        "facebook" => "Facebook"        "whatsapp" => "WhatsApp"        "reddit" => "Reddit"        "telegram" => "Telegram"        "more" => "More…"        _ => target    }// The icon: a drawing for the generic ones, the name's first letters for the sites.fn share_sheet_icon(target: str) =    match target {        "copy" => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></svg>        "email" => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2ZM22 7l-10 7L2 7"/></svg>        "more" => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13"/></svg>        "linkedin" => "in"        "whatsapp" => "WA"        "telegram" => "TG"        _ => upper(slice(share_sheet_name(target), 0, 1))    }component ShareSheet(url: str? = null, text: str? = null, targets: list = ["copy", "email", "x", "linkedin", "facebook", "whatsapp", "reddit", "telegram", "more"],                     title: str = "Share", trigger: str? = null, open: bool = false, on_share = null, on_close = null, class: str? = null) {    style global share_sheet_css    state dlg = null    state link = url ?? ""    state words = text ?? ""    state can_more = false    state copied = ""       // "", "ok" or "bad"    if copied != "" { every(1800, () => { copied = "" }) }    if open && dlg != null && !dlg.open { dlg.showModal() }    if !open && dlg != null && dlg.open && trigger == null { dlg.close() }    let shown = targets | filter((t) => t != "more" || can_more)    let copy = () => {        if js("isSecureContext") != true { copied = "bad" } else {            js("navigator.clipboard.writeText", link).then((_ = null) => {                copied = "ok"                on_share && on_share("copy")            }, (_ = null) => { copied = "bad" })        }    }    <div class={ui_cls(["ui-share-sheet-host", class])}>        {trigger != null && <button.ui-btn.ui-btn-outline type="button" aria-haspopup="dialog" @click={() => dlg.showModal()}>{trigger}</button>}        <dialog.ui-share-sheet aria-label={title}                @mount={(el) => {                    dlg = el                    if url == null { link = js("location.href") }                    if text == null { words = js("document.title") ?? "" }                    can_more = js("navigator").share != null                    if open { el.showModal() }                }}                @close={() => { on_close && on_close() }}                @click={(e) => { if e.target.tagName == "DIALOG" { dlg.close() } }}>            <div.ui-share-sheet-box>                <div.ui-share-sheet-grip aria-hidden="true"></div>                <div.ui-share-sheet-head>                    <h2>{title}</h2>                    <button.ui-share-sheet-x type="button" aria-label="Close" @click={() => dlg.close()}>                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                             aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>                    </button>                </div>                <div.ui-share-sheet-link>                    <input.ui-share-sheet-url type="text" readonly value={link} aria-label="Link"                           @focus={(e) => { e.target.select() }}/>                </div>                <ul.ui-share-sheet-grid>                    {for t in shown => <li key={t}>{t == "copy" || t == "more"                        ? <button type="button" class={ui_cls(["ui-share-sheet-target", t == "copy" && copied == "ok" && "ui-share-sheet-ok"])}                                  data-target={t}                                  @click={() => {                                      if t == "copy" { copy() } else {                                          js("navigator").share({ url: link, title: words, text: words }).then((_ = null) => {                                              on_share && on_share("more")                                              dlg.close()                                          }, (_ = null) => {})                                      }                                  }}>                            <span.ui-share-sheet-icon aria-hidden="true">{share_sheet_icon(t)}</span>                            <span>{t == "copy" && copied == "ok" ? "Link copied" : t == "copy" && copied == "bad" ? "Could not copy" : share_sheet_name(t)}</span>                          </button>                        : <a.ui-share-sheet-target href={share_sheet_href(t, link, words)} data-target={t}                             target={t == "email" ? null : "_blank"} rel={t == "email" ? null : "noopener noreferrer"}                             @click={() => {                                 on_share && on_share(t)                                 dlg.close()                             }}>                            <span.ui-share-sheet-icon aria-hidden="true">{share_sheet_icon(t)}</span>                            <span>{share_sheet_name(t)}</span>                          </a>}</li>}                </ul>                <p.ui-sr role="status">{copied == "ok" ? "Link copied" : copied == "bad" ? "Could not copy" : ""}</p>            </div>        </dialog>    </div>}
Thread
64Useful? Like it, or add to its thread below.

Thread 0 posts

On ShareSheet: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.