Examples
Basic usage
<brio-dropzone class="dropzone">
<input type="file" id="ex-attachment" accept=".png,.jpg,.jpeg,.pdf" />
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span data-prompt>Drag and drop a file here, or click to browse</span>
<span class="text-subtle">PNG, JPG, or PDF up to 10 MB</span>
</brio-dropzone>
Native form submission
<form
method="post"
enctype="multipart/form-data"
action="#"
onsubmit="return false;"
class="stack"
>
<label class="label" for="ex-form-attachment">Attachment</label>
<docs-resize>
<brio-dropzone class="dropzone">
<input type="file" id="ex-form-attachment" name="attachment" accept=".pdf" />
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span data-prompt>Drop a PDF or click to browse</span>
</brio-dropzone>
</docs-resize>
<button type="submit" class="button is-primary">Submit</button>
</form>
Validation
<brio-dropzone class="dropzone">
<input
type="file"
id="ex-invalid-input"
accept=".png,.jpg,.pdf"
aria-invalid="true"
aria-describedby="ex-invalid-msg"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<span data-prompt>Drag and drop a file here, or click to browse</span>
</brio-dropzone>
An attachment is required.
States
<div class="stack">
<docs-resize>
<brio-dropzone class="dropzone is-dragover">
<input type="file" id="ex-state-dragover" />
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span data-prompt>Drop file to upload</span>
</brio-dropzone>
</docs-resize>
<docs-resize>
<brio-dropzone class="dropzone is-accepted">
<input type="file" id="ex-state-accepted" />
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M20 6 9 17l-5-5" />
</svg>
<span data-prompt>report-q1-2026.pdf</span>
</brio-dropzone>
</docs-resize>
<docs-resize>
<brio-dropzone class="dropzone is-rejected">
<input type="file" id="ex-state-rejected" />
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
<span data-prompt>File type not accepted: sketch-draft.psd</span>
</brio-dropzone>
</docs-resize>
</div>
Source comments
Dropzone -- drag-and-drop file upload area (extra).
Slots: none. Authored content (icon, prompt, hint) goes inside as
regular markup -- the dropzone stacks its children with a snug gap.
State (per P5 -- .is-* only when no ARIA equivalent exists):
:focus-within Keyboard focus on the file input
(no JS hop -- modern selector)
:hover Pointer over the dropzone
.is-dragover A drag operation is currently
over the dropzone (set by JS)
.is-accepted Files validated and adopted
(set by JS, persists)
.is-rejected Files rejected by the input's
accept-pattern check (set by
JS, cleared by the next valid
drop or by the consumer)
input[aria-invalid="true"] Server- or JS-set invalid; paints
the dropzone border in danger,
same pattern as the .input family
Markup:
<brio-dropzone class="dropzone">
<input type="file" id="attachment" accept=".png,.jpg,.pdf">
<svg aria-hidden="true">...</svg>
<span data-prompt>
Drag and drop a file here, or click to browse
</span>
<span class="text-subtle">PNG, JPG, or PDF up to 10 MB</span>
</brio-dropzone>
The file input stays in the DOM (visually hidden, still focusable
for keyboard users). The custom element forwards clicks anywhere
on the dropzone to the input, opening the native file picker. The
data-prompt span is the JS update target -- the element rewrites
its textContent when files are accepted or rejected.
Archetype: Themed veneer (P3) over a custom element. The class
carries the look; the custom element carries the behavior. Without
the JS, the file input is hidden and the dropzone is non-functional
-- the styling presumes its companion JS is loaded.
Why a class on a custom element:
Consistent with .tabs on <brio-tabs>, .input on form atoms, etc.
The class is the "I want brio's styling" gate; the custom element
tag is the JS gate. They pair in the canonical markup but the
CSS doesn't reach for the tag selector -- the class is the API.
State remaps (declared inside the state rule) override only the
color tokens; padding / gap / radius stay constant across states.
Future considerations:
.is-image -- preview the dropped image inline (FileReader or
object URL into a thumbnail slot). Not in initial port; revisit
when an image-upload-heavy consumer needs it.
Reference
Source: src/css/extras/brio-dropzone.css
Applies to
.dropzone
Classes
| Class | Kind | Description |
|---|---|---|
.is-dragover | variant | Active drag state when a file is over the dropzone |
.is-accepted | variant | Files validated and adopted successfully |
.is-rejected | variant | Files rejected by the accept-pattern check |
Tokens
| Token | Default | Description |
|---|---|---|
--dropzone-border-color | var(--color-border-strong) | Border color for the dropzone container |
--dropzone-border-style | dashed | Border style, dashed by default to signal a drop target |
--dropzone-border-width | 2px | Border width for the dropzone container |
--dropzone-bg | var(--color-surface) | Background color for the dropzone surface |
--dropzone-color | var(--color-text-muted) | Text color for the dropzone prompt |
--dropzone-icon-color | var(--color-text-subtle) | Color for SVG icons inside the dropzone |
--dropzone-radius | var(--radius-md) | Corner radius for the dropzone |
--dropzone-padding-inline | var(--ui-pad-inline-spacious) | Inline-axis padding inside the dropzone |
--dropzone-padding-block | var(--ui-pad-block-spacious) | Block-axis padding inside the dropzone |
--dropzone-gap | var(--ui-gutter-snug) | Gap between stacked children inside the dropzone |
--dropzone-font-size | var(--ui-font-size-sm) | Font size for the prompt and hint text |
--dropzone-transition | border-color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out) | Transition shorthand for state color changes |