#border-radius
Explore tagged Tumblr posts
Text
How to Create Multi-Step Forms With Vanilla JavaScript and CSS
New Post has been published on https://thedigitalinsider.com/how-to-create-multi-step-forms-with-vanilla-javascript-and-css/
How to Create Multi-Step Forms With Vanilla JavaScript and CSS
Multi-step forms are a good choice when your form is large and has many controls. No one wants to scroll through a super-long form on a mobile device. By grouping controls on a screen-by-screen basis, we can improve the experience of filling out long, complex forms.
But when was the last time you developed a multi-step form? Does that even sound fun to you? There’s so much to think about and so many moving pieces that need to be managed that I wouldn’t blame you for resorting to a form library or even some type of form widget that handles it all for you.
But doing it by hand can be a good exercise and a great way to polish the basics. I’ll show you how I built my first multi-step form, and I hope you’ll not only see how approachable it can be but maybe even spot areas to make my work even better.
We’ll walk through the structure together. We’ll build a job application, which I think many of us can relate to these recent days. I’ll scaffold the baseline HTML, CSS, and JavaScript first, and then we’ll look at considerations for accessibility and validation.
I’ve created a GitHub repo for the final code if you want to refer to it along the way.
The structure of a multi-step form
Our job application form has four sections, the last of which is a summary view, where we show the user all their answers before they submit them. To achieve this, we divide the HTML into four sections, each identified with an ID, and add navigation at the bottom of the page. I’ll give you that baseline HTML in the next section.
Navigating the user to move through sections means we’ll also include a visual indicator for what step they are at and how many steps are left. This indicator can be a simple dynamic text that updates according to the active step or a fancier progress bar type of indicator. We’ll do the former to keep things simple and focused on the multi-step nature of the form.,
The structure and basic styles
We’ll focus more on the logic, but I will provide the code snippets and a link to the complete code at the end.
Let’s start by creating a folder to hold our pages. Then, create an index.html file and paste the following into it:
Open HTML
<form id="myForm"> <section class="group-one" id="one"> <div class="form-group"> <div class="form-control"> <label for="name">Name <span style="color: red;">*</span></label> <input type="text" id="name" name="name" placeholder="Enter your name"> </div> <div class="form-control"> <label for="idNum">ID number <span style="color: red;">*</span></label> <input type="number" id="idNum" name="idNum" placeholder="Enter your ID number"> </div> </div> <div class="form-group"> <div class="form-control"> <label for="email">Email <span style="color: red;">*</span></label> <input type="email" id="email" name="email" placeholder="Enter your email"> </div> <div class="form-control"> <label for="birthdate">Date of Birth <span style="color: red;">*</span></label> <input type="date" id="birthdate" name="birthdate" max="2006-10-01" min="1924-01-01"> </div> </div> </section> <section class="group-two" id="two"> <div class="form-control"> <label for="document">Upload CV <span style="color: red;">*</span></label> <input type="file" name="document" id="document"> </div> <div class="form-control"> <label for="department">Department <span style="color: red;">*</span></label> <select id="department" name="department"> <option value="">Select a department</option> <option value="hr">Human Resources</option> <option value="it">Information Technology</option> <option value="finance">Finance</option> </select> </div> </section> <section class="group-three" id="three"> <div class="form-control"> <label for="skills">Skills (Optional)</label> <textarea id="skills" name="skills" rows="4" placeholder="Enter your skills"></textarea> </div> <div class="form-control"> <input type="checkbox" name="terms" id="terms"> <label for="terms">I agree to the terms and conditions <span style="color: red;">*</span></label> </div> <button id="btn" type="submit">Confirm and Submit</button> </section> <div class="arrows"> <button type="button" id="navLeft">Previous</button> <span id="stepInfo"></span> <button type="button" id="navRight">Next</button> </div> </form> <script src="script.js"></script>
Looking at the code, you can see three sections and the navigation group. The sections contain form inputs and no native form validation. This is to give us better control of displaying the error messages because native form validation is only triggered when you click the submit button.
Next, create a styles.css file and paste this into it:
Open base styles
:root --primary-color: #8c852a; --secondary-color: #858034; body font-family: sans-serif; line-height: 1.4; margin: 0 auto; padding: 20px; background-color: #f4f4f4; max-width: 600px; h1 text-align: center; form background: #fff; padding: 40px; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; .form-group display: flex; gap: 7%; .form-group > div width: 100%; input:not([type="checkbox"]), select, textarea width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; .form-control margin-bottom: 15px; button display: block; width: 100%; padding: 10px; color: white; background-color: var(--primary-color); border: none; border-radius: 4px; cursor: pointer; font-size: 16px; button:hover background-color: var(--secondary-color); .group-two, .group-three display: none; .arrows display: flex; justify-content: space-between align-items: center; margin-top: 10px; #navLeft, #navRight width: fit-content; @media screen and (max-width: 600px) .form-group flex-direction: column;
Open up the HTML file in the browser, and you should get something like the two-column layout in the following screenshot, complete with the current page indicator and navigation.
Adding functionality with vanilla JavaScript
Now, create a script.js file in the same directory as the HTML and CSS files and paste the following JavaScript into it:
Open base scripts
const stepInfo = document.getElementById("stepInfo"); const navLeft = document.getElementById("navLeft"); const navRight = document.getElementById("navRight"); const form = document.getElementById("myForm"); const formSteps = ["one", "two", "three"]; let currentStep = 0; function updateStepVisibility() formSteps.forEach((step) => document.getElementById(step).style.display = "none"; ); document.getElementById(formSteps[currentStep]).style.display = "block"; stepInfo.textContent = `Step $currentStep + 1 of $formSteps.length`; navLeft.style.display = currentStep === 0 ? "none" : "block"; navRight.style.display = currentStep === formSteps.length - 1 ? "none" : "block"; document.addEventListener("DOMContentLoaded", () => navLeft.style.display = "none"; updateStepVisibility(); navRight.addEventListener("click", () => if (currentStep < formSteps.length - 1) currentStep++; updateStepVisibility(); ); navLeft.addEventListener("click", () => if (currentStep > 0) currentStep--; updateStepVisibility(); ); );
This script defines a method that shows and hides the section depending on the formStep values that correspond to the IDs of the form sections. It updates stepInfo with the current active section of the form. This dynamic text acts as a progress indicator to the user.
It then adds logic that waits for the page to load and click events to the navigation buttons to enable cycling through the different form sections. If you refresh your page, you will see that the multi-step form works as expected.
Multi-step form navigation
Let’s dive deeper into what the Javascript code above is doing. In the updateStepVisibility() function, we first hide all the sections to have a clean slate:
formSteps.forEach((step) => document.getElementById(step).style.display = "none"; );
Then, we show the currently active section:
document.getElementById(formSteps[currentStep]).style.display = "block";`
Next, we update the text that indicators progress through the form:
stepInfo.textContent = `Step $currentStep + 1 of $formSteps.length`;
Finally, we hide the Previous button if we are at the first step and hide the Next button if we are at the last section:
navLeft.style.display = currentStep === 0 ? "none" : "block"; navRight.style.display = currentStep === formSteps.length - 1 ? "none" : "block";
Let’s look at what happens when the page loads. We first hide the Previous button as the form loads on the first section:
document.addEventListener("DOMContentLoaded", () => navLeft.style.display = "none"; updateStepVisibility();
Then we grab the Next button and add a click event that conditionally increments the current step count and then calls the updateStepVisibility() function, which then updates the new section to be displayed:
navRight.addEventListener("click", () => if (currentStep < formSteps.length - 1) currentStep++; updateStepVisibility(); );
Finally, we grab the Previous button and do the same thing but in reverse. Here, we are conditionally decrementing the step count and calling the updateStepVisibility():
navLeft.addEventListener("click", () => if (currentStep > 0) currentStep--; updateStepVisibility(); );
Handling errors
Have you ever spent a good 10+ minutes filling out a form only to submit it and get vague errors telling you to correct this and that? I prefer it when a form tells me right away that something’s amiss so that I can correct it before I ever get to the Submit button. That’s what we’ll do in our form.
Our principle is to clearly indicate which controls have errors and give meaningful error messages. Clear errors as the user takes necessary actions. Let’s add some validation to our form. First, let’s grab the necessary input elements and add this to the existing ones:
const nameInput = document.getElementById("name"); const idNumInput = document.getElementById("idNum"); const emailInput = document.getElementById("email"); const birthdateInput = document.getElementById("birthdate") const documentInput = document.getElementById("document"); const departmentInput = document.getElementById("department"); const termsCheckbox = document.getElementById("terms"); const skillsInput = document.getElementById("skills");
Then, add a function to validate the steps:
Open validation script
function validateStep(step)
Here, we check if each required input has some value and if the email input has a valid input. Then, we set the isValid boolean accordingly. We also call a showError() function, which we haven’t defined yet.
Paste this code above the validateStep() function:
function showError(input, message) const formControl = input.parentElement; const errorSpan = formControl.querySelector(".error-message"); input.classList.add("error"); errorSpan.textContent = message;
Now, add the following styles to the stylesheet:
Open validation styles
input:focus, select:focus, textarea:focus outline: .5px solid var(--primary-color); input.error, select.error, textarea.error outline: .5px solid red; .error-message font-size: x-small; color: red; display: block; margin-top: 2px; .arrows color: var(--primary-color); font-size: 18px; font-weight: 900; #navLeft, #navRight display: flex; align-items: center; gap: 10px; #stepInfo color: var(--primary-color);
If you refresh the form, you will see that the buttons do not take you to the next section till the inputs are considered valid:
Finally, we want to add real-time error handling so that the errors go away when the user starts inputting the correct information. Add this function below the validateStep() function:
Open real-time validation script
function setupRealtimeValidation() nameInput.addEventListener("input", () => if (nameInput.value.trim() !== "") clearError(nameInput); ); idNumInput.addEventListener("input", () => if (idNumInput.value.trim() !== "") clearError(idNumInput); ); emailInput.addEventListener("input", () => if (emailInput.validity.valid) clearError(emailInput); ); birthdateInput.addEventListener("change", () => if (birthdateInput.value !== "") clearError(birthdateInput); ); documentInput.addEventListener("change", () => if (documentInput.files[0]) clearError(documentInput); ); departmentInput.addEventListener("change", () => if (departmentInput.value !== "") clearError(departmentInput); ); termsCheckbox.addEventListener("change", () => if (termsCheckbox.checked) clearError(termsCheckbox); );
This function clears the errors if the input is no longer invalid by listening to input and change events then calling a function to clear the errors. Paste the clearError() function below the showError() one:
function clearError(input) const formControl = input.parentElement; const errorSpan = formControl.querySelector(".error-message"); input.classList.remove("error"); errorSpan.textContent = "";
And now the errors clear when the user types in the correct value:
The multi-step form now handles errors gracefully. If you do decide to keep the errors till the end of the form, then at the very least, jump the user back to the erroring form control and show some indication of how many errors they need to fix.
Handling form submission
In a multi-step form, it is valuable to show the user a summary of all their answers at the end before they submit and to offer them an option to edit their answers if necessary. The person can’t see the previous steps without navigating backward, so showing a summary at the last step gives assurance and a chance to correct any mistakes.
Let’s add a fourth section to the markup to hold this summary view and move the submit button within it. Paste this just below the third section in index.html:
Open HTML
<section class="group-four" id="four"> <div class="summary-section"> <p>Name: </p> <p id="name-val"></p> <button type="button" class="edit-btn" id="name-edit"> <span>✎</span> <span>Edit</span> </button> </div> <div class="summary-section"> <p>ID Number: </p> <p id="id-val"></p> <button type="button" class="edit-btn" id="id-edit"> <span>✎</span> <span>Edit</span> </button> </div> <div class="summary-section"> <p>Email: </p> <p id="email-val"></p> <button type="button" class="edit-btn" id="email-edit"> <span>✎</span> <span>Edit</span> </button> </div> <div class="summary-section"> <p>Date of Birth: </p> <p id="bd-val"></p> <button type="button" class="edit-btn" id="bd-edit"> <span>✎</span> <span>Edit</span> </button> </div> <div class="summary-section"> <p>CV/Resume: </p> <p id="cv-val"></p> <button type="button" class="edit-btn" id="cv-edit"> <span>✎</span> <span>Edit</span> </button> </div> <div class="summary-section"> <p>Department: </p> <p id="dept-val"></p> <button type="button" class="edit-btn" id="dept-edit"> <span>✎</span> <span>Edit</span> </button> </div> <div class="summary-section"> <p>Skills: </p> <p id="skills-val"></p> <button type="button" class="edit-btn" id="skills-edit"> <span>✎</span> <span>Edit</span> </button> </div> <button id="btn" type="submit">Confirm and Submit</button> </section>
Then update the formStep in your Javascript to read:
const formSteps = ["one", "two", "three", "four"];
Finally, add the following classes to styles.css:
.summary-section display: flex; align-items: center; gap: 10px; .summary-section p:first-child width: 30%; flex-shrink: 0; border-right: 1px solid var(--secondary-color); .summary-section p:nth-child(2) width: 45%; flex-shrink: 0; padding-left: 10px; .edit-btn width: 25%; margin-left: auto; background-color: transparent; color: var(--primary-color); border: .7px solid var(--primary-color); border-radius: 5px; padding: 5px; .edit-btn:hover border: 2px solid var(--primary-color); font-weight: bolder; background-color: transparent;
Now, add the following to the top of the script.js file where the other consts are:
const nameVal = document.getElementById("name-val"); const idVal = document.getElementById("id-val"); const emailVal = document.getElementById("email-val"); const bdVal = document.getElementById("bd-val") const cvVal = document.getElementById("cv-val"); const deptVal = document.getElementById("dept-val"); const skillsVal = document.getElementById("skills-val"); const editButtons = "name-edit": 0, "id-edit": 0, "email-edit": 0, "bd-edit": 0, "cv-edit": 1, "dept-edit": 1, "skills-edit": 2 ;
Then add this function in scripts.js:
function updateSummaryValues() nameVal.textContent = nameInput.value; idVal.textContent = idNumInput.value; emailVal.textContent = emailInput.value; bdVal.textContent = birthdateInput.value; const fileName = documentInput.files[0]?.name; if (fileName) const extension = fileName.split(".").pop(); const baseName = fileName.split(".")[0]; const truncatedName = baseName.length > 10 ? baseName.substring(0, 10) + "..." : baseName; cvVal.textContent = `$truncatedName.$extension`; else cvVal.textContent = "No file selected"; deptVal.textContent = departmentInput.value; skillsVal.textContent = skillsInput.value || "No skills submitted"; }
This dynamically inserts the input values into the summary section of the form, truncates the file names, and offers a fallback text for the input that was not required.
Then update the updateStepVisibility() function to call the new function:
function updateStepVisibility() formSteps.forEach((step) => document.getElementById(step).style.display = "none"; ); document.getElementById(formSteps[currentStep]).style.display = "block"; stepInfo.textContent = `Step $currentStep + 1 of $formSteps.length`; if (currentStep === 3) updateSummaryValues(); navLeft.style.display = currentStep === 0 ? "none" : "block"; navRight.style.display = currentStep === formSteps.length - 1 ? "none" : "block";
Finally, add this to the DOMContentLoaded event listener:
Object.keys(editButtons).forEach((buttonId) => const button = document.getElementById(buttonId); button.addEventListener("click", (e) => currentStep = editButtons[buttonId]; updateStepVisibility(); ); );
Running the form, you should see that the summary section shows all the inputted values and allows the user to edit any before submitting the information:
And now, we can submit our form:
form.addEventListener("submit", (e) => e.preventDefault(); if (validateStep(2)) alert("Form submitted successfully!"); form.reset(); currentFormStep = 0; updateStepVisibility(); );
Our multi-step form now allows the user to edit and see all the information they provide before submitting it.
Accessibility tips
Making multi-step forms accessible starts with the basics: using semantic HTML. This is half the battle. It is closely followed by using appropriate form labels.
Other ways to make forms more accessible include giving enough room to elements that must be clicked on small screens and giving meaningful descriptions to the form navigation and progress indicators.
Offering feedback to the user is an important part of it; it’s not great to auto-dismiss user feedback after a certain amount of time but to allow the user to dismiss it themselves. Paying attention to contrast and font choice is important, too, as they both affect how readable your form is.
Let’s make the following adjustments to the markup for more technical accessibility:
Add aria-required="true" to all inputs except the skills one. This lets screen readers know the fields are required without relying on native validation.
Add role="alert" to the error spans. This helps screen readers know to give it importance when the input is in an error state.
Add role="status" aria-live="polite" to the .stepInfo. This will help screen readers understand that the step info keeps tabs on a state, and the aria-live being set to polite indicates that should the value change, it does not need to immediately announce it.
In the script file, replace the showError() and clearError() functions with the following:
function showError(input, message) const formControl = input.parentElement; const errorSpan = formControl.querySelector(".error-message"); input.classList.add("error"); input.setAttribute("aria-invalid", "true"); input.setAttribute("aria-describedby", errorSpan.id); errorSpan.textContent = message; function clearError(input) const formControl = input.parentElement; const errorSpan = formControl.querySelector(".error-message"); input.classList.remove("error"); input.removeAttribute("aria-invalid"); input.removeAttribute("aria-describedby"); errorSpan.textContent = "";
Here, we programmatically add and remove attributes that explicitly tie the input with its error span and show that it is in an invalid state.
Finally, let’s add focus on the first input of every section; add the following code to the end of the updateStepVisibility() function:
const currentStepElement = document.getElementById(formSteps[currentStep]); const firstInput = currentStepElement.querySelector( "input, select, textarea" ); if (firstInput) firstInput.focus();
And with that, the multi-step form is much more accessible.
Conclusion
There we go, a four-part multi-step form for a job application! As I said at the top of this article, there’s a lot to juggle — so much so that I wouldn’t fault you for looking for an out-of-the-box solution.
But if you have to hand-roll a multi-step form, hopefully now you see it’s not a death sentence. There’s a happy path that gets you there, complete with navigation and validation, without turning away from good, accessible practices.
And this is just how I approached it! Again, I took this on as a personal challenge to see how far I could get, and I’m pretty happy with it. But I’d love to know if you see additional opportunities to make this even more mindful of the user experience and considerate of accessibility.
References
Here are some relevant links I referred to when writing this article:
How to Structure a Web Form (MDN)
Multi-page Forms (W3C.org)
Create accessible forms (A11y Project)
#:not#Accessibility#ADD#aria#Article#Articles#attention#attributes#background#border-radius#box#box-shadow#browser#buttons#challenge#change#classes#code#Color#content#CSS#CV#dept#direction#display#email#error handling#event#Events#Exercise
2 notes
·
View notes
Text
CSS3: Revolucionando el Diseño Web en Alicante y la Comunidad Valenciana
Las Nuevas Reglas: CSS3 se Vuelve Loco CSS3 ha traído consigo una serie de propiedades y características que han revolucionado la forma en que diseñamos y animamos nuestras páginas web. Estas nuevas herramientas nos permiten crear interfaces de usuario más atractivas, interactivas y personalizadas. Exploremos algunas de las propiedades CSS3 más populares y cómo se están utilizando en Alicante y…
#@font-face#border-image#border-radius#box-shadow#gradient#hsla#outline#rgba#rotate#scale#skew#text-shadow#transform#transition#translate
0 notes
Note
For Wen!WWX AU: is A-Yao still a spy that (at the very end) brings Wen Ruohan down? And whatever is happening with all the Jins in general?
Wen Ruohan dies via spiritual backlash from the compulsion sigils that force Wei Wuxian to obey his wishes. Re: the Jins, they were conquered a few weeks after Wen Zhuliu died and aren't a big part of this AU.
#asks#reference#wrh raises wwx au#jgy is...still in yunmeng afaik#the wens never touched the cloud Recesses because lxc laid a protective array around the gusu border#it can't be breached as long as lxc remains within a 20-mile radius of the mingshi (aka where the array is anchored)#and so jgy never had the opportunity to rescue lxc
21 notes
·
View notes
Text
new round of you have to look at my neopets
#i was going to add the ones i have gif'd too but tumblr is a baby who can't handle images that big i guess#sorry this one is kinda lazy i wanted to include their names so i just screencapped my lookup with the border-radius removed#ok i did already post PartyRat technically bc of the deciding what they'd look like poll but#also Nothing gets included again bc i keep changing his customization all the time#i have another meerca named speedyrunner who is going to be badRNG's nemesis. i just haven't done anything with them yet.#*new creative post tag here*#i should like. make a tag for my neopets or something#wait i should have added a poll so i can know if people looked#but they don't have to like it.#because people are too intimidated by my neopets' amazingness to publicly like the post. i get it.
3 notes
·
View notes
Text
Fuck you *LCARSs your Art Fight*
#not art#artfight#star trek#I don't want to talk about how much time I've spent on this#Art Fight please give me border radius perms so I can make better stupid CSS themes
5 notes
·
View notes
Text
i follow a news acct on ig that reports from/about my hometown, and since there's a mexican consulate there theyve been posting about the mexican election since the consulate was overwhelmed with how many people showed up to vote. and usamericans in the comments are really struggling with the idea of expats/dual citizenship.
3 notes
·
View notes
Text
trying to make a stylus filter to fix the awful profile picture change, and I ended up making a few pixels of every image in posts disappear.
#css#the images don't even have the class I'm editing#its also a border-radius rule I'm setting#so it shouldn't even affect this.
0 notes
Text
ewwww just opened tumblr on desktop. why is everything so round and big it's ugly as hell
#hoping there'll be an xkit option for this because. yuck#why is every fucking website doing this why are they afraid of squares and straight edges#border-radius i am killing you with my bare hands#yapping
1 note
·
View note
Text
















[회원수5만명 검증된 밤문화골프여행 현지여행사]
[ 여행문의 카톡 : rentno1 ]
[필리핀관광청선정 8년연속 BEST AGENCY]
↓↓더 많은 필리핀 정보가 필요하시면 클릭해 주세요↓↓ 필맨스토리 필리핀골프여행
#font-family: 'Arial'#FFB6C1#87CEFA); padding: 30px; border-radius: 15px; box-shadow: 0 4px 8px rgba(0#0#0.1);#color:#FF4500; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0#0.2);#1E90FF; text-decoration: underline; font-weight: bold;#background: linear-gradient(135deg#FFD700#FF8C00); color: white; font-size: 22px; padding: 15px 30px; border: none; border-radius: 50px; cursor: pointer; font-weight: bold; box-shad#0.2); transition: all 0.3s ease;#<div style=>#<p style=>#[회원수5만명 검증된 밤문화골프여행 현지여행사]#</p>#32CD32; font-size: 20px; font-weight: bold; text-shadow: 1px 1px 3px rgba(0#[ 여행문의 카톡 : <span style=>rentno1</span> ]#FF6347; font-size: 20px; font-weight: bold; text-shadow: 1px 1px 3px rgba(0#[필리핀관광청선정 8년연속 BEST AGENCY]#FF1493; font-size: 22px; font-weight: bold; margin-top: 20px; text-shadow: 2px 2px 4px rgba(0#↓↓더 많은 필리핀 정보가 필요하시면 클릭해 주세요↓↓#<a href=“https://cafe.naver.com/philmanlove” target=“_blank” style=“text-decoration: none;”>#<button style=>#필맨스토리 필리핀골프여행#</button>#</a>#</div>
0 notes
Note
omg wait i love ur neocities !! i think ive been following you from even b4 i followed ur tumblr.. my neocities is lambbones if u wanna be net neighbors !! :D
FREAK YEAH!!!! omg :DDDD i put ur button on my draft n it'll be there in my next update :)))))
#my asks#thats so cool.................. crying#ALSO UR WEBSITE KICKS ASS WTF!!!!!!!#SO COOL IM STEALING UR BORDER RADIUS >:)))))
1 note
·
View note
Text
Tailwind’s @apply Feature is Better Than it Sounds
New Post has been published on https://thedigitalinsider.com/tailwinds-apply-feature-is-better-than-it-sounds/
Tailwind’s @apply Feature is Better Than it Sounds


By this point, it’s not a secret to most people that I like Tailwind.
But, unknown to many people (who often jump to conclusions when you mention Tailwind), I don’t like vanilla Tailwind. In fact, I find most of it horrible and I shall refrain from saying further unkind words about it.
But I recognize and see that Tailwind’s methodology has merits — lots of them, in fact — and they go a long way to making your styles more maintainable and performant.
Today, I want to explore one of these merit-producing features that has been severely undersold — Tailwind’s @apply feature.
What @apply does
Tailwind’s @apply features lets you “apply” (or simply put, copy-and-paste) a Tailwind utility into your CSS.
Most of the time, people showcase Tailwind’s @apply feature with one of Tailwind’s single-property utilities (which changes a single CSS declaration). When showcased this way, @apply doesn’t sound promising at all. It sounds downright stupid. So obviously, nobody wants to use it.
/* Input */ .selector @apply p-4; /* Output */ .selector padding: 1rem;
To make it worse, Adam Wathan recommends against using @apply, so the uptake couldn’t be worse.
Confession: The `apply` feature in Tailwind basically only exists to trick people who are put off by long lists of classes into trying the framework.
You should almost never use it 😬
Reuse your utility-littered HTML instead.https://t.co/x6y4ksDwrt
— Adam Wathan (@adamwathan) February 9, 2020
Personally, I think Tailwind’s @apply feature is better than described.
Tailwind’s @apply is like Sass’s @includes
If you have been around during the time where Sass is the dominant CSS processing tool, you’ve probably heard of Sass mixins. They are blocks of code that you can make — in advance — to copy-paste into the rest of your code.
To create a mixin, you use @mixin
To use a mixin, you use @includes
// Defining the mixin @mixin some-mixin() color: red; background: blue; // Using the mixin .selector @include some-mixin(); /* Output */ .selector color: red; background: blue;
Tailwind’s @apply feature works the same way. You can define Tailwind utilities in advance and use them later in your code.
/* Defining the utility */ @utility some-utility color: red; background: blue; /* Applying the utility */ .selector @apply some-utility; /* Output */ .selector color: red; background: blue;
Tailwind utilities are much better than Sass mixins
Tailwind’s utilities can be used directly in the HTML, so you don’t have to write a CSS rule for it to work.
@utility some-utility color: red; background: blue;
<div class="some-utility">...</div>
On the contrary, for Sass mixins, you need to create an extra selector to house your @includes before using them in the HTML. That’s one extra step. Many of these extra steps add up to a lot.
@mixin some-mixin() color: red; background: blue; .selector @include some-mixin(); /* Output */ .selector color: red; background: blue;
<div class="selector">...</div>
Tailwind’s utilities can also be used with their responsive variants. This unlocks media queries straight in the HTML and can be a superpower for creating responsive layouts.
<div class="utility1 md:utility2">…</div>
A simple and practical example
One of my favorite — and most easily understood — examples of all time is a combination of two utilities that I’ve built for Splendid Layouts (a part of Splendid Labz):
vertical: makes a vertical layout
horizontal: makes a horizontal layout
Defining these two utilities is easy.
For vertical, we can use flexbox with flex-direction set to column.
For horizontal, we use flexbox with flex-direction set to row.
@utility horizontal display: flex; flex-direction: row; gap: 1rem; @utility vertical display: flex; flex-direction: column; gap: 1rem;
After defining these utilities, we can use them directly inside the HTML. So, if we want to create a vertical layout on mobile and a horizontal one on tablet or desktop, we can use the following classes:
<div class="vertical sm:horizontal">...</div>
For those who are new to Tailwind, sm: here is a breakpoint variant that tells Tailwind to activate a class when it goes beyond a certain breakpoint. By default, sm is set to 640px, so the above HTML produces a vertical layout on mobile, then switches to a horizontal layout at 640px.
If you prefer traditional CSS over composing classes like the example above, you can treat @apply like Sass @includes and use them directly in your CSS.
<div class="your-layout">...</div>
.your-layout @apply vertical; @media (width >= 640px) @apply horizontal;
The beautiful part about both of these approaches is you can immediately see what’s happening with your layout — in plain English — without parsing code through a CSS lens. This means faster recognition and more maintainable code in the long run.
Tailwind’s utilities are a little less powerful compared to Sass mixins
Sass mixins are more powerful than Tailwind utilities because:
They let you use multiple variables.
They let you use other Sass features like @if and @for loops.
@mixin avatar($size, $circle: false) width: $size; height: $size; @if $circle border-radius: math.div($size, 2);
On the other hand, Tailwind utilities don’t have these powers. At the very maximum, Tailwind can let you take in one variable through their functional utilities.
/* Tailwind Functional Utility */ @utility tab-* tab-size: --value(--tab-size-*);
Fortunately, we’re not affected by this “lack of power” much because we can take advantage of all modern CSS improvements — including CSS variables. This gives you a ton of room to create very useful utilities.
Let’s go through another example
A second example I often like to showcase is the grid-simple utility that lets you create grids with CSS Grid easily.
We can declare a simple example here:
@utility grid-simple display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); gap: var(--gap, 1rem);
By doing this, we have effectively created a reusable CSS grid (and we no longer have to manually declare minmax everywhere).
After we have defined this utility, we can use Tailwind’s arbitrary properties to adjust the number of columns on the fly.
<div class="grid-simple [--cols:3]"> <div class="item">...</div> <div class="item">...</div> <div class="item">...</div> </div>
To make the grid responsive, we can add Tailwind’s responsive variants with arbitrary properties so we only set --cols:3 on a larger breakpoint.
<div class="grid-simple sm:[--cols:3]"> <div class="item">...</div> <div class="item">...</div> <div class="item">...</div> </div>
This makes your layouts very declarative. You can immediately tell what’s going on when you read the HTML.
Now, on the other hand, if you’re uncomfortable with too much Tailwind magic, you can always use @apply to copy-paste the utility into your CSS. This way, you don’t have to bother writing repeat and minmax declarations every time you need a grid that grid-simple can create.
.your-layout @apply grid-simple; @media (width >= 640px) --cols: 3;
<div class="your-layout"> ... </div>
By the way, using @apply this way is surprisingly useful for creating complex layouts! But that seems out of scope for this article so I’ll be happy to show you an example another day.
Wrapping up
Tailwind’s utilities are very powerful by themselves, but they’re even more powerful if you allow yourself to use @apply (and allow yourself to detach from traditional Tailwind advice). By doing this, you gain access to Tailwind as a tool instead of it being a dogmatic approach.
To make Tailwind’s utilities even more powerful, you might want to consider building utilities that can help you create layouts and nice visual effects quickly and easily.
I’ve built a handful of these utilities for Splendid Labz and I’m happy to share them with you if you’re interested! Just check out Splendid Layouts to see a subset of the utilities I’ve prepared.
By the way, the utilities I showed you above are watered-down versions of the actual ones I’m using in Splendid Labz.
One more note: When writing this, Splendid Layouts work with Tailwind 3, not Tailwind 4. I’m working on a release soon, so sign up for updates if you’re interested!
#ADD#Advice#approach#Article#Articles#avatar#background#Blue#border#border-radius#Building#classes#code#Color#columns#CSS#CSS Grid#css preprocessors#desktop#direction#display#easy#effects#English#Features#framework#gap#grid#grid-template-columns#grids
0 notes
Text
0 notes
Text
chris sturniolo smut
little, lacy set
warnings: unprotected sex, established relationship, kinda rough ( just a little ), not proofread.
inspired by this thing that he said.
chris’s posture stiff but he is laid back against the couch, manspreading as he watched you laugh and giggle with nick right in front of him.
that wasn’t the issue at all though, it was the way your pyjamas slid off your waist to your hip bone with every small move your body made, giving him limited view of your dainty, pretty, lacy panties.
his eyes shamelessly drop to where the thin fabric sat on your skin, he gulps taking the sight in.
his mind immediately going places, thoughts invaded by imagination of just how perfect your pussy must look in those. he wonders if you have the whole set on or just the panties, either way he’d like to confirm. he’d like to see you in that little, lacy set.
as nick grows tired and eventually yawns his way out of the living room, leaving chris alone with you. finally- he thinks.
a smirk grows on chris’s face, his eyes darkening as watches you get up from your spot to come sit beside him.
“why’d you come here? go sit all the way there, so far away from me giggling and shit” he stops you mid way, chris’s tone remains serious but his expression gives away the playfulness of his words.
“baby we were in the same room” you justify your actions, your actions of not being in his one meter radius for a small moment.
chris rolls his eyes at you, his body turning to face you now. he stretches his hand forward, tugging one of his fingers in the waistband of your pyjamas, pulling you close in a swift motion, his face right in front of your belly.
he looks up at you with a small smile before nuzzling his face into the exposed skin your crop top gives him access to in front of him. your hands habitually tangle in his hair, as he rubs his nose in your stomach and groans loudly.
“whats the issue?” a small yet prominent frown displayed on your face when he looks up at you with hooded eyes and shakes his head before putting his face back in your tummy.
his hands grip your sides as he continues to groan and pull you closer into his face. you can feel his breath settle when he leaves his mouth open, slowly kissing and sucking on the skin right above your hipbone, right above the waistband of your panties.
you can feel his sharp teeth slowly take the fabric in his mouth, pulling it down just a little, a soft gasp leaves your lips as his mouth leaves sloppy and messy kisses beneath the small piece of cloth he just pulled down.
“you know you’ve been torturing me this whole time?” his words muffled against your soft skin. your grip tightens in his hair.
he looks up at you, his bottom lip jutted out and rested on your skin. his eyes dark with desire.
he moves his hands to the border of pyjamas, pulling them down just enough to give him the full view of your underwear, chris’s lets out a sound-something between a whine and a groan.
his head tilts and he presses his face right on your core, his nose nudging your folds through the lace. his eyes shut as he breaths you in, slowly opening his mouth and pecking your clothed centre. his tongue pokes out with each one of the kisses, he can already feel the wetness seep through the panties onto his taste buds.
your thighs quivering and a shiver runs down your spine with each one of his actions. his hand slowly makes its way up from you hip and roams around the rest of your body. in a fleet motion he swings your body onto the couch, your head flops down with no warning-making you yelp.
“stay quite” his words almost unheard between his hasty moves, one of his his hands grip your hip and the other pulls on your shirt. his kisses get sloppier by the second, your pants pooling around your knees, falling down unnoticed.
he pulls away to quickly get rid of your shirt, your face and hair disheveling in the process.
he props his head to get a good look, you are wearing the whole set.
“fuck.” chris’s mouth slightly agape taking in the sight of you in a lacy lingerie set sprawled out on the couch, he’d seen you in one before but it’ll never have less of an effect on him.
“y’knew what you were doing wearing this huh?” his voice husky, huskier than when he wakes up.
“now you gotta stay quite for me ma, okay?” you nod, your heart racing and breathing ragged.
he settles between your knees, his hands fiddling with his belt and zipper.
the rate at which he is moving shows you just how much he wants you right now, your breathing gets caught in your throat when he successfully pulls down both his pants and boxers in 0.2 seconds.
his hard cock springs free, bobbing around in air before he props himself on one elbow above you, his fingers sliding your panties off to the side, picking up some residue of your wetness in the process.
he aligns himself with your entrance, pushing in at once with a deep thrust, an involuntary whimper passes through his mouth. bottom lip caught between his teeth.
your mouth forms an “o” with the way its open, holding back screams and moans.
“get this off” chris’s eyes land on your bra.
you try your best to unbuckle it in the position you are in right now.
“just—fucking do it!” chris’s hands fly to your bra straps pulling them down as he whisper shouts in your face, his spit landing on your cheek.
once he can see your tits in front-he starts moving, thrusting slowly at first making sure they don’t make too much noise.
the couch creaks and shifts a little as chris increases his pace, your eyes roll back and his hand flies to your mouth.
“you like getting fucked like this huh? like wearing slutty lace for me?” his nose pushing into yours as he moves, thats how close his face is.
“answer me” he looks at you in the eyes, wanting you to nod.
and you do. your face scrunched up in pleasure as he increases the speed of his thrusts, plunging into you with everything in him, not caring about the sounds created by the skin contact.
“god i could fuck you all day…” his eyes drop to the way your tits bounces with every thrust, trailing back to your fucked out features and the lower half of your face that’s covered with his hand, he presses on to your mouth even more feeling his stomach curl at the way your your tight walls take him in every time he bottoms out.
you can feel the pressure building in your lower belly rapidly, he pulls his propped elbow up to wrap your legs around his waist. your eyes shut in pleasure, a deep frown on your face.
“fuck -you feel so good ma, so fucking good” he can feel you clench around him at his words, his eyes fluttering at the sensation.
“gonna cum already? cum for me ma, look at me” breathless words flow out of his mouth, his hips snapping into yours with force.
he can feel you tightly pucker your lips under his palm, holding back the loud screech he knows you let out when you cum.
your legs shake around his torso, and you cum around his dick. head thrown back, back arched and hips bucked.
“fu-fuck fuc-” his voice low as he tries to contain himself, following right behind you with his orgasm and filling you up.
he pants through the last few thrusts to get rid of both of your highs.
“fuck..” he exhales pulling out of you slowly, watching his cum spill over your panties and thighs, making sort of a mess on the couch he shares with his brothers but he could care less about that right now.
he finally pulls his hand off your mouth, and you let out a deep breath from your drool covered mouth, chest heaving.
“i have definitely had a dream about this before” he chuckles softly looking at you in awe.
taglist: @espressqe @ginswife @sturnsburna @carolina454 @hope2244 @hotgirlbl0gger @violetstxrniolo777 @riggysworld @verycoolmiyah @fadedstvrn @purpledreamertyphoon @mattsplaything @whore4chris @chris-halleluja @annsx03 @mattsdemi @chrislittleslut @poolover123 @luvvnai @chrissturniolossidehoe @pompomprrin @harmonysturniolo @sofia-is-a-sturniolo-triplet-fan @soph-loren @ccsturns @lovesturni0l0s @chriss-slutt @wysmols @sturniolosluttt @mattsdillion @alyssa-sturn @bilssturns @sturnobessed @mxnsonn @izzylovesmatt @sturniolosymphony @chrissturnioloswife88 @sxphiee3 @purpledreamertyphoon @whoreforchrissturnniolo @slutformatt17 @realuvrrr @sweetxcheeryx @sturnl0ve @estellesdoll @glitterybtch @courta13 @mattsbitchh @slvtf0rchr1s @trevorsgodmother
#chris sturniolo#christopher sturniolo#matt sturniolo#chris sturiolo fanfic#chris x reader#chris sturniolo smut#chris imagine#nick sturniolo#chris smut#matt sturniolo texts#matt sturniolo x reader#matt stuniolo fanfic#matthew sturniolo#matt x reader#sturniolo#sturniolo x reader#sturniolo smut#sturniolo triplets#nicolas sturniolo#sturniolo fanfic
1K notes
·
View notes
Text
i'll say this again: i fucking despise border radius where it's the most unnecessary crap 194230849083204032 times
for the love of god. why
#sy.txt#something inside me dies every time i see border radius specifically where it's not desired whatsoever
0 notes
Text
Glowy dark mode site skin






🎼 You would not believe your eyes, if 10 million fireflies ended up in the header of your AO3. 🎶
It's been a while since I tried glow effects, but I saw the fireflies and I couldn't resist.
CSS code under the cut.
#header { background-image: url("https://cdn.pixabay.com/photo/2022/10/19/16/56/fireflies-7533056_1280.jpg"); background-repeat: no-repeat; background-size: cover; background-position: center center; background-color: #152623; }
#header .heading { height: 15em; }
#header .primary { background: #0d1d1f; box-shadow: 0px 0px 15px #f9f6ce; }
#header .logo, #header .heading sup { visibility: hidden; }
#header .heading a { color: #152623; text-shadow: 0px 0px 15px #f9f6ce; }
.event .userstuff { background: #425e50; border: 1px solid #f9f6ce; }
#outer.wrapper { background: #0d1d1f; color: #f9f6ce; }
#main a { color: #8c9b76; }
#greeting a.dropdown-toggle, #header .actions a { color: #f9f6ce !important; text-shadow: 0px 0px 3px #152623; }
#greeting .menu, #header .dropdown .menu, #header .dropdown:hover a { background: #0d1d1f; box-shadow: 0px 0px 15px #f9f6ce; }
span.submit.actions input.button { display: none; }
#greeting img.icon { display: none; }
#header #search .text, .search [role="tooltip"] { background: #0d1d1f; box-shadow: 0px 0px 15px #f9f6ce; color: #f9f6ce !important; border: 1px solid #0d1d1f; }
form.search input[type=text], form.search input[type=submit], .autocomplete div.dropdown ul { background: #0d1d1f !important; border: none; box-shadow: 0px 0px 15px #f9f6ce; color: #f9f6ce; display: block; }
#header #search .text { width: 7em; }
.notice, .comment_notice, .kudos_notice, ul.notes, .caution, .error, .comment_error, .kudos_error, .alert.flash { background: #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce !important; color: #0d1d1f; border: none; }
.notice a, .comment_notice a, .kudos_notice a, ul.notes a, .caution a, .error a, .comment_error a, .kudos_error a, .alert.flash a { color: #506957; font-weight: bold; }
.splash .module h3 { color: #f9f6ce; border-bottom: 2px solid #f9f6ce; }
.splash .favorite li:nth-of-type(2n+1) a { background: #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; color: #0d1d1f; font-weight: bold; font-variant: small-caps; }
.splash .favorite li:nth-of-type(2n+2) a { color: #f9f6ce; font-weight: bold; font-variant: small-caps; font-size: 110%; }
.splash .favorite li:nth-of-type(2n+1) a:hover, .splash .favorite li:nth-of-type(2n+2) a:hover { color: #f9f6ce; font-weight: bold; font-variant: small-caps; background: #425e50; }
#footer { background: #425e50; color: #f9f6ce; border-top: 3px solid #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
#footer a, #footer .heading { color: #f9f6ce; }
.actions a, .actions a:focus, .actions input:focus, .action:focus, .actions li input, .actions li input[type="submit"], input[type="submit"], .actions li label, ul.navigation.actions li a, .action:link, .actions a:link { background: #425e50; border: 1px solid #f9f6ce; color: #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; border-radius: 5px; }
.current, #dashboard .current { background: #f9f6ce !important; color: #0d1d1f !important; box-shadow: 0px 0px 15px #f9f6ce !important; border-radius: 5px; }
#dashboard.own { border-top: 5px solid #f9f6ce; border-bottom: 5px solid #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
#dashboard a:hover { background: #0d1d1f; box-shadow: 0px 0px 15px #f9f6ce; }
#dashboard a { color: #f9f6ce; }
dl.meta { border: 1px solid #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
.listbox .index { background: #0d1d1f; }
.listbox, fieldset fieldset.listbox { background: #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
form dl, fieldset, fieldset fieldset, fieldset fieldset fieldset, fieldset fieldset dl dl, dd.hideme, form blockquote.userstuff, input, select, select:focus, textarea, span.symbol.question, .own { background: #0d1d1f !important; color: #f9f6ce !important; border: 1px solid #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
.autocomplete li.added, .post .meta dd ul li.added, label, label.required { color: #f9f6ce; }
span.delete { background: #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
span.delete a { color: #0d1d1f !important; font-weight: bold; }
.ui-sortable li, .dynamic form, div.dynamic { background: #0d1d1f; border: 1px solid #f9f6ce; }
.dropdown { background: #0d1d1f; }
form.verbose legend, .verbose form legend { background: #f9f6ce; color: #0d1d1f; box-shadow: 0px 0px 15px #f9f6ce; }
li.blurb { border: 1px solid #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
.draft { background: #0d1d1f; color: #f9f6ce; border: 2px dashed #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
.draft .wrapper { background: #0d1d1f; border: 1px solid #f9f6ce; }
#header h2 { background: #f9f6ce !important; color: #0d1d1f; box-shadow: 0px 0px 15px #f9f6ce; }
#stat_chart svg rect:first-of-type { opacity: 60%; }
#stat_chart g[clip-path^=url] > g:nth-of-type(2) rect, #stat_chart svg g:nth-of-type(2) > g rect:last-of-type, #stat_chart g[clip-path^=url] > g:nth-of-type(2) rect:first-of-type { filter: hue-rotate(140deg); opacity: 80% !important; }
.statistics .index li:nth-of-type(2n) { background: #0d1d1f; border: 1px solid #f9f6ce; }
.reading h4.viewed, dl.index dd, table, th, dt.child { background: #0d1d1f; }
#modal, span.replied { background: #0d1d1f; color: #f9f6ce; border: 2px solid #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
h4.heading.byline { background: #f9f6ce; color: #0d1d1f; }
li.comment { border: 1px solid #f9f6ce; }
.comment div.icon { border-bottom: 5px solid #f9f6ce; box-shadow: 0px 0px 15px #f9f6ce; }
.thread .even { background: #425e50; }
.unread { background: #0d1d1f; border: 5px dashed #f9f6ce !important; }
span.unread { background: #f9f6ce; color: #0d1d1f; }
span.indicator::before { box-shadow: 0px 0px 15px #f9f6ce; }
.warnings .tag, .work .warning a.tag, dd.warning.tags a { border: 1px solid #f9f6ce; border-radius: 5px; background: #f9f6ce; padding-left: 2px; padding-right: 2px; box-shadow: 0px 0px 10px #f9f6ce; }
.relationships .tag, .work .relationships a.tag, dd.relationship.tags a { background: none; color: #f9f6ce !important; font-weight: bold; text-shadow: 0px 0px 15px #f9f6ce; }
.filters .expander { background: url("https://64.media.tumblr.com/3c89981f933f9f57157d6dcec6fd85a7/94c6737c6db9ad60-e5/s1280x1920/f7557e617a5439c506721bd326580a0cb4c1f8d8.png") left center no-repeat; color: #f9f6ce !important; font-weight: bold; }
.filters .expanded .expander { background: url("https://64.media.tumblr.com/dab095a2fd9387bc1e0c57747ba6b13f/94c6737c6db9ad60-ad/s1280x1920/c1a4e14e0565cdcac5d3e20bebac3ab440f2d607.png") left center no-repeat; }
5K notes
·
View notes
Note
Hewwo; just know your awesome sauce. Thank you for existing
💜
ty, nonnie! thank you for existing, too
#asks#love to see people spreading kindness in the world#in a world full of common cruelty to be kind is punk#or however that quote goes#im on desktop rn and damnnn tumblr ux team really said border radius on these tags huh
0 notes