Skip to content

Posting a photo

The whole flow for adding a photo: a floating button opens a sheet, the post form takes the photo and its details, and the page gets one event with everything in it.

JS
form.js, tag-input.js, dropzone.js, post-form.js, sheet.js

01 — Try it

The flow

Press the + button in the bottom-right corner, or open the same form as a tearsheet or a modal. Close it with unsaved changes to see the guard, or make the upload fail to see the error.

Event log · jewel:post

Nothing posted yet.

Markup

The floating button, then the sheet holding the post form. Listen for jewel:post and call waitUntil() with your upload.

HTML
<body class="has-fab">
  …
  <button class="fab" type="button" aria-label="New post" data-sheet-open="post-sheet">
    <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
  </button>

<dialog class="sheet sheet--bottom" id="post-sheet" data-component="sheet" aria-labelledby="post-sheet-title">
  <div class="sheet__panel panel panel--content">
    <header class="sheet__head">
      <h2 class="sheet__title label" id="post-sheet-title">New post</h2>
      <button class="btn btn--ghost btn--icon" type="button" data-sheet-close aria-label="Close">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>
      </button>
    </header>
    <div class="sheet__body">
      <form class="form" id="post-form" data-component="form post-form" novalidate data-success="Posted." aria-labelledby="post-sheet-title">
        <div class="field">
          <label class="field__label" for="post-photo">Photo<span class="field__req" aria-hidden="true">*</span></label>
          <div class="dropzone knockout" data-component="dropzone" data-max-size="20">
            <input class="dropzone__input" id="post-photo" name="photo" type="file" accept="image/*" required>
            <label class="dropzone__empty" for="post-photo">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" aria-hidden="true"><rect x="3" y="5" width="18" height="14"/><path d="m3 16 5-5 4 4 3-3 6 6"/><circle cx="16" cy="9" r="1.5"/></svg>
              <span class="dropzone__title">Drop a photo, or <u>browse</u></span>
              <span class="dropzone__hint">One photo · up to 20 MB</span>
            </label>
            <figure class="dropzone__preview" hidden><img alt=""><figcaption class="dropzone__meta"></figcaption></figure>
            <div class="dropzone__actions" hidden>
              <button class="btn btn--ghost" type="button" data-dropzone-replace>Replace</button>
              <button class="btn btn--text" type="button" data-dropzone-remove>Remove</button>
            </div>
          </div>
        </div>
        <div class="field">
          <label class="field__label" for="post-title">Title<span class="field__req" aria-hidden="true">*</span></label>
          <input class="input" id="post-title" name="title" type="text" required maxlength="80" data-counter aria-describedby="post-title-count" placeholder="Name the photo">
          <div class="field__meta"><p class="field__counter" id="post-title-count"></p></div>
        </div>
        <div class="field">
          <label class="field__label" for="post-description">Description <span class="field__optional">(optional)</span></label>
          <textarea class="input" id="post-description" name="description" rows="3" maxlength="500" data-counter aria-describedby="post-description-count" placeholder="Where, when, what it means"></textarea>
          <div class="field__meta"><p class="field__counter" id="post-description-count"></p></div>
        </div>
        <div class="form__row form__row--2">
          <div class="field">
            <label class="field__label" for="post-location">Location <span class="field__optional">(optional)</span></label>
            <input class="input" id="post-location" name="location" type="text" autocomplete="off" placeholder="Dayton, Ohio">
          </div>
          <div class="field">
            <label class="field__label" for="post-tags">Tags <span class="field__optional">(up to 10)</span></label>
            <div class="tag-input" data-component="tag-input" data-max="10" data-suggestions='["Dayton","Graffiti","Pond","Towers"]'>
              <ul class="tag-input__list" aria-label="Tags"></ul>
              <input class="tag-input__text" id="post-tags" type="text" placeholder="Add a tag">
              <input type="hidden" name="tags">
            </div>
          </div>
        </div>
        <div class="field">
          <label class="field__label" for="post-alt">Alt text <span class="field__optional">(optional)</span> <span class="field__badge" hidden>Suggested</span></label>
          <textarea class="input" id="post-alt" name="alt" rows="2" aria-describedby="post-alt-hint" placeholder="Describe the photo for people who can’t see it"></textarea>
          <p class="field__hint" id="post-alt-hint">Never blocks posting. A suggestion may appear here after you pick a photo; edit or clear it.</p>
        </div>
        <label class="choice"><input type="checkbox" role="switch" name="publish" checked> Publish to site</label>
        <p class="form__error" role="alert" hidden></p>
        <p class="form__status" role="status" hidden></p>
        <div class="form__actions">
          <button class="btn btn--text" type="button" data-sheet-close>Cancel</button>
          <button class="btn" type="submit" data-busy-label="Posting…">Post</button>
        </div>
      </form>
    </div>
  </div>
</dialog>

  <script>
    document.getElementById('post-form').addEventListener('jewel:post', (e) => {
      e.detail.waitUntil(upload(e.detail.formData));   // your code
    });
  </script>
</body>

02 — The event

jewel:post

DetailWhat it holds
fileThe photo, as a File.
title, description, location, altStrings.
tagsAn array of strings.
publishTrue or false, from the switch.
formData, formFor sending as is.
waitUntil(promise)Keeps the form busy. Resolve to show “Posted.” and reset; reject to show the error and keep the draft.

03 — Built from

04 — Guidelines

Use it for

  • Ask for the photo first; everything else follows from it.
  • Make alt text easy: suggest a draft, never block posting on it.
  • Keep the draft when an upload fails, and say how to retry.

Avoid

  • Required fields beyond the photo and a title.
  • Closing the sheet on a failed upload.

New post

    Never blocks posting. A suggestion may appear here after you pick a photo; edit or clear it.