<form id="idea" class="idea-form" action="">
<svg class="idea-form__icon" viewBox="0 0 32 32" width="32px" height="32px" aria-hidden="true">
<g fill="currentcolor">
<path d="M16,2A10,10,0,0,0,6,12a9.19,9.19,0,0,0,3.46,7.62c1,.93,1.54,1.46,1.54,2.38h2c0-1.84-1.11-2.87-2.19-3.86A7.2,7.2,0,0,1,8,12a8,8,0,0,1,16,0,7.2,7.2,0,0,1-2.82,6.14c-1.07,1-2.18,2-2.18,3.86h2c0-.92.53-1.45,1.54-2.39A9.18,9.18,0,0,0,26,12,10,10,0,0,0,16,2Z" />
<rect x="11" y="24" width="10" height="2"/>
<rect x="13" y="28" width="6" height="2"/>
</g>
</svg>
<button class="idea-form__btn idea-form__btn--start" type="button" data-toggle>I have an idea</button>
<div class="idea-form__fill"></div>
<div class="idea-form__content">
<label for="my-idea" class="idea-form__label">Idea</label>
<textarea id="my-idea" class="idea-form__textarea" placeholder="My idea is…"></textarea>
<button class="idea-form__btn" type="submit" disabled>Submit</button>
</div>
</form>
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--hue: 53;
--bg: hsl(var(--hue),10%,90%);
--fg: hsl(var(--hue),10%,10%);
--primary: hsl(var(--hue),90%,50%);
--trans-dur: 0.3s;
--trans-timing1: cubic-bezier(0.65,0,0.35,1);
--trans-timing2: cubic-bezier(0.65,0,0.35,1.35);
font-size: calc(14px + (30 - 14) * (100vw - 280px) / (3840 - 280));
}
body,
button,
textarea {
font: 1em/1.5 Montserrat, sans-serif;
}
body {
background-color: var(--bg);
color: var(--fg);
display: flex;
height: 100vh;
transition:
background-color var(--trans-dur),
color var(--trans-dur);
}
.idea-form {
background-color: hsl(0,0%,0%);
color: hsl(0,0%,100%);
margin: auto;
overflow: hidden;
position: relative;
width: 16.5em;
height: 4.5em;
transition:
background-color var(--trans-dur),
color var(--trans-dur),
height var(--trans-dur) var(--trans-timing2);
&,
&__btn,
&__content {
border-radius: 1.5em;
}
&,
&__btn {
position: relative;
}
&__btn,
&__textarea {
outline: transparent;
transition:
background-color var(--trans-dur),
box-shadow var(--trans-dur),
opacity var(--trans-dur) var(--trans-timing1);
appearance: none;
appearance: none;
tap-highlight-color: transparent;
&:disabled {
cursor: not-allowed;
opacity: 0.3;
}
}
&__btn {
background-color: hsl(0,0%,0%);
box-shadow: 0 0 0 0.25em hsla(var(--hue),90%,30%,0);
color: hsl(0,0%,100%);
cursor: pointer;
display: flex;
margin-inline-start: auto;
padding: 0.5em 1em;
z-index: 2;
&--start {
background-color: transparent;
box-shadow: none;
color: currentColor;
display: flex;
align-items: center;
letter-spacing: 0.0625em;
padding-inline-start: 5.25em;
width: 100%;
height: 4.5em;
text-transform: uppercase;
transition:
opacity var(--trans-dur) var(--trans-timing1),
visibility var(--trans-dur) steps(1,start);
}
&:focus-visible {
box-shadow: 0 0 0 0.25em hsla(var(--hue),90%,30%,1);
}
}
&__content {
opacity: 0;
padding: 1.5em 1em 0.75em;
padding-inline-start: 5.25em;
position: absolute;
top: 0;
left: 0;
visibility: hidden;
transition:
opacity var(--trans-dur) var(--trans-timing1),
visibility var(--trans-dur) steps(1,end);
}
&__fill,
&__icon,
&__label {
position: absolute;
}
&__fill {
background-color: hsl(var(--hue),90%,50%);
border-radius: 50%;
top: 1.75em;
left: 2em;
width: 1em;
height: 1em;
transform: translateY(50%) scale(0);
transition: transform var(--trans-dur) var(--trans-timing1);
[dir="rtl"] & {
right: 2em;
left: auto;
}
}
&__btn--start:focus-visible {
box-shadow: none;
}
&__btn--start:focus-visible + &__fill,
&__btn--start:hover + &__fill {
transform: translateY(0) scale(1);
}
&__icon {
color: currentColor;
display: block;
top: 0.75em;
left: 1em;
width: 3em;
height: 3em;
z-index: 1;
[dir="rtl"] & {
right: 1em;
left: auto;
}
}
&__label {
overflow: hidden;
width: 1px;
height: 1px;
}
&__textarea {
background-color: transparent;
color: hsl(0,0%,0%);
display: block;
margin-bottom: 0.75em;
resize: none;
width: 100%;
height: 3em;
&::placeholder {
color: hsl(var(--hue),10%,50%);
}
}
// expanded
&[data-expanded="true"] {
background-color: transparent;
height: 8.5em;
transition-timing-function: steps(1,end), ease, var(--trans-timing2);
}
&[data-expanded="true"] &__btn--start {
opacity: 0;
pointer-events: none;
transition-timing-function: var(--trans-timing1), steps(1,end);
visibility: hidden;
}
&[data-expanded="true"] &__content {
opacity: 1;
transition-timing-function: var(--trans-timing1), steps(1,start);
visibility: visible;
}
&[data-expanded="true"] &__fill {
transform: translateY(0) scale(32);
}
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root {
--bg: hsl(var(--hue),10%,10%);
--fg: hsl(var(--hue),10%,90%);
}
.idea-form {
background-color: hsl(0,0%,100%);
color: hsl(0,0%,0%);
}
}
View Compiled
window.addEventListener("DOMContentLoaded",() => {
const ideaForm = new IdeaForm("#idea");
});
class IdeaForm {
/** Form used for this component */
form: HTMLFormElement | null;
/** Timeout function for submission */
timeout = 0;
private _idea = "";
get idea(): string {
return this._idea;
}
set idea(value: string) {
this._idea = value;
const submitBtn = this.form?.querySelector("[type=submit]") as HTMLButtonElement;
if (submitBtn) {
submitBtn.disabled = value.length === 0;
}
}
private _expanded = false;
get expanded(): boolean {
return this._expanded;
}
set expanded(value: boolean) {
this._expanded = value;
this.form?.setAttribute("data-expanded",`${value}`);
}
private _state = SubmitState.Default;
get state() {
return this._state;
}
set state(value: SubmitState) {
this._state = value;
const textarea = this.form?.querySelector("#my-idea") as HTMLTextAreaElement;
const submitBtn = this.form?.querySelector("[type=submit]") as HTMLButtonElement;
if (textarea) {
textarea.disabled = value !== SubmitState.Default;
}
if (submitBtn) {
if (value === SubmitState.Sending) {
submitBtn.textContent = Label.Sending;
submitBtn.disabled = true;
} else if (value === SubmitState.Done) {
submitBtn.textContent = Label.Sent;
} else {
submitBtn.textContent = Label.Submit;
}
}
}
/**
* @param el CSS selector of the form
*/
constructor(el: string) {
this.form = document.querySelector(el);
this.form?.addEventListener("click",this.toggle.bind(this));
this.form?.addEventListener("input",this.ideaUpdate.bind(this));
this.form?.addEventListener("submit",this.ideaSubmit.bind(this));
document.addEventListener("click",this.outsideToCollapse.bind(this));
document.addEventListener("keydown",this.escToCollapse.bind(this));
}
/**
* Click outside the form to collapse.
* @param e Click event
* */
outsideToCollapse(e: Event): void {
if (this.state !== SubmitState.Default) return;
let parent: HTMLElement | null = e.target as HTMLElement;
while (parent !== null) {
if (parent === this.form) {
return;
}
parent = parent.parentElement;
}
this.expanded = false;
}
/**
* Hide the form by pressing Esc.
* @param e Keyboard event
* */
escToCollapse(e: KeyboardEvent): void {
if (e.code === "Escape" && this.state === SubmitState.Default) {
this.expanded = false;
}
}
/**
* Show or hide the form.
* @param e Click event
* */
toggle(e: Event): void {
const button = e.target as HTMLButtonElement;
if (button.hasAttribute("data-toggle")) {
this.expanded = !this.expanded;
if (this.expanded) {
const textarea = this.form?.querySelector("#my-idea") as HTMLTextAreaElement;
textarea?.focus();
}
}
}
/**
* Submit the idea content.
* @param e Submit event
* */
async ideaSubmit(e: SubmitEvent): Promise<void> {
e.preventDefault();
if (this.state !== SubmitState.Default) return;
const delaySending = 1000;
const delayDone = 600;
const delayReset = 300;
this.state = SubmitState.Sending;
return await new Promise<void>(resolve => {
// send
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
resolve();
},delaySending);
}).then(async () => {
// submitted
this.state = SubmitState.Done;
return await new Promise<void>(resolve => {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
resolve();
},delayDone);
});
}).then(() => {
// collapse and reset
this.expanded = false;
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.form?.reset();
this.idea = "";
this.state = SubmitState.Default;
},delayReset);
});
}
/**
* Update the idea content internally.
* @param e Input event
* */
ideaUpdate(e: Event): void {
const textarea = e.target as HTMLTextAreaElement;
this.idea = textarea.value;
}
}
const enum Label {
Sending = "Sending…",
Sent = "Sent",
Submit = "Submit"
}
const enum SubmitState {
Default = 0,
Sending,
Done
}
View Compiled
This Pen doesn't use any external JavaScript resources.