Part Six: Building a Profile Page with HTML and CSS: A form, A Dialog, and Netlify
In this part, we will add a form to our profile page, use a dialog to display our form, deploy our page using Netlify, and set up Netlify to handle form submissions.
Published on: 2025-02-17
Written by Schalk Neethling
Welcome to part six of the series where we are building a profile page with HTML and CSS. This is going to be an exciting one!
In this part, we will add a form to our profile page, use a dialog to display our form, deploy our page using Netlify, and lastly set up Netlify to handle form submissions. That’s a lot to cover, so let’s get started.
Note: Remember to start a new feature branch preferably associated with an issue on GitHub before you start coding.
Trigger and <dialog>
We are going to follow a mobile-first approach so be sure to start your server, open the page, and get your developer tools ready and switched to responsive mode. If you need a refresher, please refer to part three of the series.
The first thing to do is to add our <button>
(or trigger) which we will use to open the <dialog>
element.
Tip: If you ever wonder whether to use a link or a button, remember: “Buttons do things (trigger actions) and links go places.” In this case, we are doing something, so we use a button.
Add the following code to your index.html
file after the closing <hgroup>
tag:
<button type="button" id="show-dialog" class="button button-primary">
Get in touch
</button>
We need to create a couple of color custom properties in our CSS file to style our button. Add the following code to your variables.css
file:
--color-orange-50: #ffb657;
--color-orange-90: #281d0e;
Sometimes we can use these custom properties directly, but in this instance, mapping --color-orange-50
to --color-brand-highlight
allows us to swap out what color we want to use for our highlight color and therefore we are not tying ourselves to a specific color name. Add the following code to your variables.css
file:
--color-brand-highlight: var(--color-orange-50);
Referring to our design in Figma you will notice that the button has 16px
of padding on the inline (left and right) axis and 8px
on the block (top and bottom) axis. Our button also has a border radius of 8px
. Here again, it is a good idea to create a dedicated custom property for the border radius of our buttons instead of using --size-8
directly. We also need a add a custom property for a transparent border. I will discuss a little later why we need this. Add the following code to your variables.css
file:
--border-transparent: var(--size-1) solid transparent;
--border-radius-button: var(--size-8);
While we are at it, also add the following two sizes to variables.css
:
--size-1: 0.0625rem;
--size-2: 0.125rem;
Now we can style our button. Create a new file called buttons.css
and add the following code to the file:
.button {
/* to ensure a border is visible on Windows high-contrast mode */
border: var(--border-transparent);
border-radius: var(--border-radius-button);
cursor: pointer;
display: flex;
font-size: var(--typography-font-size-default);
justify-content: center;
max-inline-size: max-content;
padding: var(--size-8);
}
.button-primary {
background-color: var(--color-brand-highlight);
padding-inline: var(--size-16);
}
There is not a lot that is new here, but there are a few items worth discussing. First, why did I add a transparent border to the base button
class? In short, this is to ensure that your buttons are distinguishable on Windows high-contrast mode. It is common to remove the border from buttons when taking over control of styling. However, without the transparent border, Windows high-contrast mode will not show a border at all, which could make it hard for users to distinguish buttons from other UI elements. By adding a transparent border, Windows will map the color of the border to the system default in high-contrast mode ensuring that our buttons will always stand out.
You can see a quick demo of this in the video below. Here I am using the Chrome Developer Tools’ emulation mode to emulate Window high-contrast mode. This does not show all of the options available to users in Windows high-contrast mode, but it does give you a good idea of the problem I described above.
If you were quick, you might have noticed that switching to forced colors (high-contrast) mode surfaced a bug in our implementation! If you did not, take another look and focus specifically on the LinkTree component. Our icons disappear in high-contrast mode. This is why it is so important to always test our work in various configurations such as high-contrast mode to ensure that our designs are accessible to all users.
Why is this happening? This is because we are setting our icons using the background-image
property in CSS and in high-contrast mode, the background-image
property is forced to none
. As a result, our icons disappear. I was a little sneaky here in order to demonstrate three things:
- Do not rely on icons (or color) alone whenever possible.
- You now know how to set an SVG (or other image format) as a background image and you know when not to do so. Note: When used with descriptive text, the icons being hidden in high-contrast mode is not a problem.
- If you must use icons by themselves, embed the SVG directly in the HTML.
We will address this bug at the end of this post, but feel free to give it a go yourself.
Back to our button. I set the cursor
to pointer
(the little hand with a pointing finger icon) as this is what users expect when hovering over a button. I also like to set the display
of my buttons to flex
as this opens up several possibilities you will most likely reach for when styling buttons.
- If you want to center the content inside the button, you can do so with
justify-content: center;
andalign-items: center;
if needed. - If you want to add an icon to your button, you can easily set the desired gap between the icon and the text and,
- If you want to be able to have an icon either at the start or the end, you can easily accomplish this using
flex-direction: row-reverse;
.
The max-inline-size: max-content
style is not required and can be omitted depending on your design requirements. In this instance, it felt like the appropriate style for the button. I then ensure that the font-size
matches the design and give the button a base padding
of 0.5rem
.
In the primary button class, we merely override the background-color
and padding-inline
to match our design. We are not done yet though. While the design does not show it, we need to also take care of a couple of interaction states. Add the following code to your buttons.css
file:
.button:focus-visible {
border-color: var(--color-neutral-10);
outline: var(--size-2) solid var(--color-neutral-80);
outline-offset: var(--size-2);
}
.button-primary:hover,
.button-primary:focus {
background-color: var(--color-orange-30);
}
We start by defining a global rule for the focus-visible
state. This selector allows us top apply a specific style when an element (a button in this instance) is focused and the browser has determined that the user is not using a pointing device such as a keyboard. To quote from the specification:
While the :focus pseudo-class always matches the currently-focused element, UAs only sometimes visibly indicate focus (such as by drawing a “focus ring”), instead using a variety of heuristics to visibly indicate the focus only when it would be most helpful to the user. The :focus-visible pseudo-class matches a focused element (or pseudo-element) in these situations only, allowing authors to change the appearance of the focus indicator without changing when a focus indicator appears.
To test this, use the tab key on your keyboard to move focus to the button. The second set of style rules when then apply when the button is hovered over or focused. To test these you will need to exit out of the mobile view as mobile devices do not have a hover state.
Let’s add the <dialog>
element to our page. Add the following code to your index.html
file after the closing </main>
tag:
<dialog>
Baseline Widely available
Supported in Chrome: yes.
Supported in Edge: yes.
Supported in Firefox: yes.
Supported in Safari: yes.
This feature is well established and works across many devices and browser versions. It’s been available across browsers since March 2022
<dialog id="dialog-contact-form"></dialog>
That is pretty much it to be honest, the browser takes care of most of the rest. There are however a few things we need to do. I want to pause for a moment here and set expectations. I will not do a deep dive into the <dialog>
element here. If you wish to learn all of the details along with when and when not to use the <dialog>
element, I recently wrote a detailed post on the <dialog>
element.
To open the dialog, we need to add a little JavaScript. Create a new folder called js
and a file called dialog.js
and add the following code to the file:
const dialogForm = document.querySelector("#dialog-contact-form");
const trigger = document.querySelector("#show-dialog");
trigger.addEventListener("click", () => {
dialogForm.showModal();
});
Just before the closing <body>
tag in your index.html
file, add the following code to import the JavaScript file:
<script src="/js/dialog.js"></script>
This is a very simple script. We first get a reference to the dialog element and the trigger button. We then add an event listener to the trigger button that listens for a click
event. When the button is clicked, we call the showModal()
method on the dialog element. This method is what opens the dialog. If you try it out now, the dialog should open when you click the button. The dialog will also receive focus and if you press the escape key, the dialog will close and move focus back to the element that opened the dialog. These are all things you as a developer would have to implement yourself if you were to create a dialog before the <dialog>
element was introduced.
It is also advisable to add a button to the dialog to close it. To enable this, we will need a bit of HTML and CSS, no JavaScript. Add the following code to your index.html
file inside the <dialog>
element:
<dialog id="dialog-contact-form">
<form method="dialog">
<button class="button button-close" type="submit">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
role="img"
>
<path
d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"
/>
</svg>
<span class="visually-hidden">Close dialog</span>
</button>
</form>
</dialog>
The first thing to notice is that the form has a method
attribute set to dialog
. This is a new value for the method
attribute and has special meaning to the browser. It essentially tells that browser that when this form is submitted, there is no data to submit and it should instead close the dialog that matches the current context. As such, all that the form contains is a button with a type
of submit
. You will also notice that we embed the SVG in the HTML here as we are relying on the icon alone and do not want it to disappear in high-contrast mode. We also add a visually-hidden
class to the text that reads “Close dialog”. You will remember from an earlier post that this class is used to hide text visually but keep it available to screen readers.
Note: Now that we have add an interactive element (the close button) to the
<dialog>
element, you will notice that it receives focus and not the dialog itself. This is as intended and is done by the browser using internal heuristics.
When you open the dialog now, you will see the close button. If you click the button, the dialog will close as expected. There is something else that is added when a dialog is shown like this. The browser will add a backdrop behind the dialog and also make the page content inert. This means that the user cannot interact with the page content until they close the dialog. You can test this by trying to click any of the links in the LinkTree while the dialog is open.
While the backdrop isn’t visible yet, we have full control over its styling. Add the following code to your variables.css
file:
--dialog-backdrop: rgba(0 0 0 / 50%);
Then add a new CSS file called dialog.css
and add the following code to the file:
dialog::backdrop {
background: rgba(0 0 0 / 50%);
}
Remember to import it into the main CSS file. I kept this simple, but feel free to experiment and find a style you like. If you open the dialog now, you will see a semi-transparent black backdrop behind the dialog. This adds a good visual cue to the user and wraps up the <dialog>
.
The Contact Form
Before we add the contact form, let’s add a heading to our dialog. We will also associate the heading with the dialog element itself so that this dialog will be uniquely identifiable to screen readers. Add the following code to your index.html
file inside the <dialog>
element, just after the previous <form>
element:
<h2 id="dialog-contact-form-title">Get in touch</h2>
Now update the <dialog>
element to include the aria-labelledby
attribute and associate it with the heading we just added. Add the following code to your index.html
file inside the <dialog>
element:
<dialog
aria-labelledby="dialog-contact-form-title"
id="dialog-contact-form"
></dialog>
Onto the form. Add the following code to your index.html
file inside the <dialog>
element, just after the previous <h2>
element:
<form name="contact" method="post" action="/">
<fieldset>
<legend>Enter your details (all fields are required)</legend>
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea rows="5" id="message" name="message" required></textarea>
</div>
<button type="submit" class="button button-primary">Send message</button>
</fieldset>
</form>
Because we will be sending data somewhere, we are using the POST
method for the form. We also set the action
attribute to /
which is the root of our site. This is a placeholder for now, but we will update this later when we set up Netlify to handle form submissions. Next, we wrap the form elements in a fieldset
element (as these all make up a cohesive unit) and add a legend
element to provide a descriptive title for the form. We also take the opportunity to ensure that the user understands that all of the fields are required in order to submit the form.
We wrap each group of fields in a div
element with a class of form-group
. This is a little utility I use a lot that we will add to our CSS in a moment. We also add a label
element for each input field. The for
attribute of the label
element is associated with the id
attribute of the form field. This is important for screen readers as it allows them to associate the label with the form field. We also set the required
attribute on each form field to ensure that the user cannot submit the form without filling in all of the fields.
Lastly we add a submit button to the form. This button is styled the same as our other buttons and has a type
of submit
. This is important as it tells the browser that this button should submit the form when clicked. If you click the button now, the form will submit and the page will refresh, but nothing is happening just yet.
Let’s add the CSS for the form-group
utility class to our utils.css
file:
/**
* A utility class used on a wrapper element such as a `div` to group two form elements
* commonly a label and an input.
* - Sets the display to grid.
* - Sets the a 0.25rem gap between the elements.
* - Sets the margin block end to 0.5rem.
*/
.form-group {
display: grid;
gap: var(--size-4);
margin-block-end: var(--size-8);
}
Let us tighten this all up some more by taking it from the top. On small screen devices such as phones, we probably want to dialog to take over the entire display. To do this, add the following to your dialog.css
file:
@media screen and (width < 48rem) {
dialog {
block-size: 100vh;
inline-size: 100vw;
}
}
This is where we are going to start encountering the idea of shared-first CSS. We are still using a mobile-first approach as these two approaches are not mutually exclusive but can work really well together.
Tip: You can read all about the shared-first approach for CSS on the blog of my colleague Michael Großklaus.
Next we want our close button to be positioned to the right (or end) of the dialog. We could use absolute positioning, but we can achieve the same with Flexbox. Add the following code to your dialog.css
file:
[method="dialog"] {
display: flex;
justify-content: end;
}
We select the appropriate <form>
element by using an attribute selector that we know will be unique to the form containing the close button. By setting the display
of the form to flex
and the justify-content
to end
, the button will be pushed to the end of the dialog. We are also using a local property value here and as a result this will adjust appropriately should the writing orientation change from left-to-right to right-to-left.
Next we want to remove a bit of margin from the heading so, let’s start by giving it a class:
<h2 class="dialog-heading" id="dialog-contact-form-title">Get in touch</h2>
Then add the following code to your dialog.css
file:
.dialog-heading {
margin-block-end: 0;
}
Next, let’s cleanup the fieldset
and legend a little. Add the following to forms.css
:
fieldset {
border: 0;
padding-inline: 0;
}
legend {
margin-block-end: var(--size-8);
}
And lastly, let’s add some spacing between the form fields and the submit button. Add the following to forms.css
:
dialog .button {
margin-block-start: var(--size-16);
}
Great! Now our dialog is looking pretty good. If you open the dialog now, you will see that it takes up the entire screen on small devices and that the close button is positioned to the right of the dialog. The form is also looking good. Go head and switch over to a larger viewport size as as the iPad Mini (1024px by 768px).
When you open the dialog now, you will see that the dialog is not taking up the entire screen. This demonstrates the shared-first approach to CSS. Instead of setting the previous rules as the default for the <dialog>
and then needing to override it for larger viewports, we specifically targeted smaller viewports and so, when we switch to a larger viewport, the style rules are simply no longer applied.
However, you will notice that our trigger button is not aligned correctly. Because we used grid on the container, this is an easy fix. Add the following to your about.css
file:
@media screen and (width > 47.9375rem) {
.about-me-header {
place-items: center;
}
}
Here again, we can see that there is nothing to override on smaller viewport (or from smaller viewports), we are specifically targeting larger viewports. the <dialog>
does feel a little cramped so, let’s change that:
@media screen and (width > 47.9375rem) {
dialog {
inline-size: 30rem;
}
}
One small tweak to make the <dialog>
pop a little on these larger viewports. Add the following custom property to your variables.css
file:
--dialog-box-shadow: 0 0 1rem rgba(0 0 0 / 30%);
Then add the following to your dialog.css
file:
@media screen and (width > 47.9375rem) {
dialog {
box-shadow: var(--dialog-box-shadow);
inline-size: 30rem;
}
}
Note: But what are we sharing? We are sharing the CSS properties that are not dependent on the viewport size. This is a very simple example, but as you start to build more complex layouts, you will find that you can share a lot more than you might initially think. In fact, you could clean things up a little and move the styling for the backdrop into the media query targeting larger viewports as it is not needed on smaller viewports where the dialog takes up the entire screen. In other words, these styles are note technically needed to be shared.
Deploying to Netlify
Now that we have everything ready, let’s deploy our site to Netlify. If you do not have a Netlify account, you can sign up for free at netlify.com using your GitHub account. Once you have signed up, we first need to push out latest changes to GitHub and merge everything to main
.
Note: If you chose to not go the GitHub route, you can use Netlify Drop and drag and drop your folder to deploy it. I would highly encourage you to explore doing this using GitHub as it will make your life a lot easier in the long run.
Once you have signed up and merged everything into main
, log into Netlify and click the “Add new site” button and select, “Import existing project” from the dropdown. On the next screen choose GitHub. A window will open where you will have to authorize Netlify and give it access to your GitHub repositories. You can choose to give it access to all of your repositories or only choose the repository containing your profile page.
Once you see the repository in the list, click on it. You can leave everything at the default settings or choose a custom name for your site. Once you are ready, click on the deploy button. On the next page you will see the build process happening. Once the build is complete, you will see a link to your site. Click on the link to open your site in a new tab.
Whoa! Your page is live on the internet. Give yourself a pat on the back. Well done.
Netlify Forms
Note: On the free plan you get 100 form submissions per month. One this is exceeded you will need to upgrade to one of the paid plans. You can find more information on the Netlify pricing page.
Now, in the sidebar you will see a link called forms. When you click on this link you will be taken to a page where you can enable form detection for the site. Go ahead and do so. With forms support enabled, we can finish the remaining steps.
Tip: There are a few ways you can handle form submissions with Netlify. While we will do so using JavaScript, there are also simpler and much more involved options. Please refer to the Netlify forms documentation to learn more about the options available to you.
We will start by adding the data-netlify="true"
attribute to the form element. This is what tells Netlify that this form should be handled by their form submission service. Add the following to your index.html
file as part of the contact form’s <form>
element:
Reminder: Remember to create a new feature branch before continuing.
<form name="contact" method="post" action="/" data-netlify="true"></form>
Next, in the js
folder add a file named form-handler.js
and add the following code to the file:
const form = document.querySelector("[name='contact']");
form.addEventListener("submit", async (event) => {
event.preventDefault();
try {
const formData = new FormData(form);
const response = await fetch(form.getAttribute("action"), {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(formData).toString(),
});
if (response && response.ok) {
form.reset();
}
} catch (error) {
console.error(error);
}
});
Next, import the JavaScript file inside your index.html
:
<script src="/js/form-handler.js"></script>
This series is not about JavaScript, but I do not want you to walk away feeling that you do not have any idea what you just did so, let’s walk through the code. We start by getting a reference to the form element. We then add an event listener to the form that listens for a submit
event. When the form is submitted, we prevent the default behavior of the form (which is to submit the form and refresh the page). We then create a new FormData
object from the form and send it to the server using the fetch
API.
Because things can go wrong when we do this, we wrap everything in a try...catch
block. If everything goes well, we reset the form. If something goes wrong, the code inside the catch
will be executed which will log the error to the console (Note: This is not something you want to do on a production website).
We can do little bit more though, right? Let’s add a little feedback to the user. Add the following code to your index.html
file between the heading and the form:
<p class="form-success" hidden>
Your information has been successfully submitted. Thank you.
</p>
<p class="form-critical" hidden>
Oh no! Something went wrong. Please try again.
</p>
Here we use the HTML hidden
attribute to hide both of these elements by default. We will show one or the other based on the outcome of the form submission. Next, update the form-handler.js
file to show the appropriate message based on the outcome of the form submission:
const form = document.querySelector("[name='contact']");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const formCritical = document.querySelector(".form-critical");
const formSuccess = document.querySelector(".form-success");
formSuccess.hidden = true;
formCritical.hidden = true;
try {
const formData = new FormData(form);
const response = await fetch(form.getAttribute("action"), {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(formData).toString(),
});
if (response && response.ok) {
form.reset();
formSuccess.hidden = false;
}
} catch (error) {
formCritical.hidden = false;
}
});
Here we get references to the success and error messages and hide them in case one of them is currently shown. We then show the appropriate message based on the outcome of the form submission. If the form submission is successful, we show the success message. If something goes wrong, we show the error message. We have now also removed the console.error
which is not something you want to show to users, even if it is hidden in the developer tools.
Let’s add some CSS to make those message a bit more presentable. Add the following custom properties to your variables.css
file:
--color-green-90: #054e3b;
--color-green-10: #d1fae5;
--color-red-90: #450a0a;
--color-red-10: #fecaca;
--color-success-light: var(--color-green-10);
--color-success-dark: var(--color-green-90);
--color-critical-light: var(--color-red-10);
--color-critical-dark: var(--color-red-90);
--border-success: var(--size-1) solid var(--color-success-dark);
--border-critical: var(--size-1) solid var(--color-critical-dark);
The inside forms.css
add the following code:
.form-critical,
.form-success {
padding-block: var(--size-4);
padding-inline: var(--size-8);
}
.form-critical {
background-color: var(--color-critical-light);
border: var(--border-critical);
color: var(--color-critical-dark);
}
.form-success {
background-color: var(--color-success-light);
border: var(--border-success);
color: var(--color-success-dark);
}
Because of how Netlify Forms work (it requires a build process on Netlify) we cannot test this locally. Because what we are building here is not mission critical and the world will not blow up if our form does not work, we can go ahead and commit, push, and merge our work. If you are deploying from GitHub, then a deployment will automatically be kicked of as soon as you merge to main
. If you are using Netlify Drop, you can drag and drop your folder again to deploy the changes.
In both instances, keep and eye on your deployment status using the site’s dashboard. Once the deployment has succeeded, go ahead and try to send yourself a message. If all goes well 🤞 you should see the success message show up and the form being reset. Go back to Netlify and click on the link to your forms in the sidebar.
Under active forms you should see one called “contact”. Depending on how many times you submitted the form, there will be one or more submissions. Click on the form and you will be shown a list of submissions. Clicking on any of these will show the details of the message. You can also delete message from there or mark them as spam.
Tip: You can also set up various types of notification for form submissions if you do not always want to check your messages using the Netlify admin. You can learn more about this in the Netlify forms documentation.
Wow! We have achieved a lot in this post and over the course of the series. There is one more thing to do. Let’s fix the bug with those icons in our LinkTree component. Remember to pull down everything so your main
branch is in sync with the remote. Then create a new feature branch and let’s get started.
Fixing the LinkTree Icons
Because you may want to refer back to how we did the icons as background images, we will keep the icons.css
file around and simply remove the import from main.css
. Once you have done this, you will notice that the icons are no longer visible, as expected. Inside your index.html
file, update all of the links in the LinkTree to resemble the one below:
<li>
<a
class="linktree-link"
href="https://github.com/schalkneethling"
rel="external"
>
<svg width="25" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)">
<path
d="M20.03 7.095a5.605 5.605 0 0 0-.505-4.47.75.75 0 0 0-.65-.375 5.601 5.601 0 0 0-4.5 2.25h-2.25a5.601 5.601 0 0 0-4.5-2.25.75.75 0 0 0-.65.375 5.604 5.604 0 0 0-.504 4.47A5.45 5.45 0 0 0 5.75 9.75v.75a5.255 5.255 0 0 0 4.541 5.2A3.731 3.731 0 0 0 9.5 18v.75H7.25A2.25 2.25 0 0 1 5 16.5a3.75 3.75 0 0 0-3.75-3.75.75.75 0 1 0 0 1.5A2.25 2.25 0 0 1 3.5 16.5a3.75 3.75 0 0 0 3.75 3.75H9.5v1.5a.75.75 0 1 0 1.5 0V18a2.25 2.25 0 0 1 4.5 0v3.75a.75.75 0 1 0 1.5 0V18c0-.833-.278-1.643-.791-2.3a5.255 5.255 0 0 0 4.541-5.2v-.75a5.451 5.451 0 0 0-.72-2.655Zm-.78 3.405a3.75 3.75 0 0 1-3.75 3.75H11a3.75 3.75 0 0 1-3.75-3.75v-.75c.01-.75.234-1.482.647-2.107A.75.75 0 0 0 8 6.922a4.107 4.107 0 0 1 .074-3.149 4.114 4.114 0 0 1 3.03 1.881.75.75 0 0 0 .63.346h3.032a.75.75 0 0 0 .632-.346 4.112 4.112 0 0 1 3.03-1.88 4.108 4.108 0 0 1 .072 3.148.758.758 0 0 0 .094.717c.417.626.645 1.359.656 2.111v.75Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" transform="translate(.5)" d="M0 0h24v24H0z" />
</clipPath>
</defs>
</svg>
<span class="visually-hidden">Collaborate with me on GitHub</span>
</a>
</li>
All I did here was to embed the SVG directly inside the anchor element. You will notice that the color of the icons are not quite what we had and need. When a link has not yet been visited it is a bright blue, and if we clicked on any of the links before, it has a purple color. This is because we previous controlled this using the background-color
property in combination with the mask-image
property. We can still control the color of the icon using CSS. Add the following code to your variables.css
file:
--color-link-color: var(--color-brand-primary);
Then add the following code to your main.css
file:
a:link,
a:visited {
color: var(--color-link-color);
text-decoration: none;
}
And let’s have a little fun to close out this post. Add the following to main.css
just after the code from above:
a:hover svg,
a:focus svg {
scale: 1.2;
transition: scale 0.2s ease-in-out;
}
This will scale the icon up by 20% when you hover over it or focus on it. This is a nice little touch that adds a bit of interactivity to the page. Also go ahead and test your changes using forced colors mode using your Chrome developer tools or your testing method of choice. If you are happy with the end result, go ahead and commit, push, and merge your changes.
And that is it for this post. I hope you learned a lot over the past six posts and please do share your pages with me using either Mastodon, BlueSky, or LinkedIn. I would love to see what you made. Now the big question. Is this it? Well, sort off. There will be one more post in this series in which I will cover some formatting and linting tools that will make your life and code a little easier.
I will also give you a quick whirlwind introduction to end-to-end testing using Playwright. Until then, happy coding and remember to always be kind to yourself and others.