@use 'sprucecss/scss/spruce' as *;
.file-group-container {
container: file-group-container / inline-size;
}
.file-group {
$this: &;
@include generate-variables($form-control, $include: ('border-width', 'border-radius'));
align-items: center;
border: config('border-width', $form-control) solid color('border', 'form');
border-radius: config('border-radius', $form-control);
display: flex;
gap: spacer('m');
overflow: hidden;
padding: spacer('s');
@container file-group-container (inline-size < 30rem) {
flex-direction: column;
}
+ .file-group,
.form-group + .form-group & {
margin-block-start: 0;
}
&:has([style*='background-image']) {
#{$this}__remove {
display: inline-flex;
}
#{$this}__input {
display: none;
}
}
&__preview {
align-items: center;
aspect-ratio: 1;
background-color: color-alpha('primary', 0.08);
background-position: center;
background-size: cover;
border-radius: config('border-radius', $form-control);
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
inline-size: 9rem;
justify-content: center;
@container file-group-container (inline-size < 30rem) {
aspect-ratio: 16 / 9;
inline-size: 100%;
}
&[style*='background-image'] #{$this}__icon {
display: none;
}
}
&__body {
inline-size: 100%;
@include layout-stack('xxs');
}
&__icon {
--size: 2rem;
block-size: var(--size);
color: color('primary');
inline-size: var(--size);
}
&__title {
color: color('heading');
font-weight: 700;
}
&__meta {
list-style: none;
padding-inline-start: 0;
> * + * {
margin-block-start: 0;
}
}
&__action {
display: flex;
flex-wrap: wrap;
gap: spacer('xs');
margin-block-start: spacer('xs');
}
&__remove {
display: none;
}
&__input {
flex: 1;
}
}
<div class="form-group">
<label class="form-label" for="product-image">Product image</label>
<div class="file-group-container">
<div class="file-group" data-file-group>
<div class="file-group__preview" data-file-group-preview>
<svg class="file-group__icon" aria-hidden="true" focusable="false" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
</div>
<div class="file-group__body">
<p class="file-group__title" data-file-group-name>Upload a product image</p>
<ul class="file-group__meta" id="product-image-help">
<li>JPG or PNG, max. 5 MB</li>
<li>At least 1200 × 1200 pixels</li>
</ul>
<div class="file-group__action">
<input class="form-file file-group__input" id="product-image" type="file" name="product-image" accept="image/png, image/jpeg" aria-describedby="product-image-help" data-file-group-input>
<button class="btn btn--soft-danger btn--sm file-group__remove" type="button" data-file-group-remove>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18 6 6 18M6 6l12 12"/></svg>
Remove image
</button>
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label" for="product-image-filled">Product image (with an image)</label>
<div class="file-group-container">
<div class="file-group" data-file-group>
<div class="file-group__preview" style="background-image: url('/img/blog/about-modern-sass.png')" data-file-group-preview>
<svg class="file-group__icon" aria-hidden="true" focusable="false" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
</div>
<div class="file-group__body">
<p class="file-group__title" data-file-group-name>product-photo.png</p>
<ul class="file-group__meta" id="product-image-filled-help">
<li>JPG or PNG, max. 5 MB</li>
<li>At least 1200 × 1200 pixels</li>
</ul>
<div class="file-group__action">
<input class="form-file file-group__input" id="product-image-filled" type="file" name="product-image-filled" accept="image/png, image/jpeg" aria-describedby="product-image-filled-help" data-file-group-input>
<button class="btn btn--soft-danger btn--sm file-group__remove" type="button" data-file-group-remove>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18 6 6 18M6 6l12 12"/></svg>
Remove image
</button>
</div>
</div>
</div>
</div>
</div>
<p class="sr-only" aria-live="polite" data-file-group-status></p>
(() => {
const status = document.querySelector('[data-file-group-status]');
document.querySelectorAll('[data-file-group]').forEach((group) => {
const input = group.querySelector('[data-file-group-input]');
const preview = group.querySelector('[data-file-group-preview]');
const name = group.querySelector('[data-file-group-name]');
const remove = group.querySelector('[data-file-group-remove]');
const emptyText = 'Upload a product image';
let url = null;
input.addEventListener('change', () => {
const [file] = input.files;
if (!file) return;
if (url) URL.revokeObjectURL(url);
url = URL.createObjectURL(file);
preview.style.backgroundImage = `url('${url}')`;
name.textContent = file.name;
status.textContent = `${file.name} selected.`;
remove.focus();
});
remove.addEventListener('click', () => {
if (url) URL.revokeObjectURL(url);
url = null;
input.value = '';
preview.style.removeProperty('background-image');
name.textContent = emptyText;
status.textContent = 'Image removed.';
requestAnimationFrame(() => input.focus());
});
});
})();