Frontend Sprint: 100 Quick Tips on HTML, CSS and JavaScript

100 useful tricks, best practices and free tips on HTML, CSS and JavaScript for new and continuing frontend developers.

Semantics and accessibility

1. The most popular HTML tags are div and span. These are ordinary containers with no hidden meaning.

2. Previously, they were used to design everything and everywhere, but the HTML5 standard brought many more suitable tags.

3. You now have about 110 tags to choose from. The probability that you will not be able to find one that is suitable is very small.


4. When choosing a text selection method, think carefully about the meaning you put into it.

5. For example, the b and strong tags look the same (bold text), but the first one does not carry any semantic meaning, and the second one emphasizes the importance of the highlighted fragment.

6. The same situation with i and em. These are italic fonts, but em (emphasis) places a logical emphasis on the text, but i does not.

7. For headings, use h1 — h6.

8. If the selected text is not of semantic importance, it is better to use for it a regular span. For example, you can select icons in this way.


9. When creating a new structural component, focus on its semantics and select the appropriate elements.

10. For headers and footers, header and footer tags are intended. On one page there can be a lot of them, for example, the footer of the site with contact details and the footer of articles with information about the author.

11. But the main in document should be only one – it contains unique information that is different from page to page. Usually the markup looks like this:


12. For the semantic content design, use the article and section tags.

13. In a nutshell, an article is self-contained content like an article, note, or comment that can be easily moved somewhere else, for example, to share on social networks.

14. And section is a separate thematic section of the document, as a rule, with its own title.

15. Inside the article you can place the address block with contact information.

16. The main navigation page is made in the form of a tag nav.

17. Another useful tag is noscript. Its role is to provide feedback for users with disabled JavaScript.

18. Be sure to review all existing HTML elements in the documentation.

19. Many semantic elements have their own default styles, which may differ from your design. Do not be afraid to change them in your CSS.

20. Consider the hierarchy and relationship of the elements. Each tag has allowed content and valid parent containers. You can find out which ones in the MDN documentation.

21. For example, earlier inside a tag it was impossible to place block elements and headers. In order to preserve the validity of the document, it was necessary to make a link to the child element and absolutely position it over the parent.

22. In the current standard, the a tag can itself serve as a container for other elements besides links, and this is very convenient.


23. To improve the semantics and even greater accessibility of the project, you can use a set of ARIA attributes.

24. For example, for modal windows, the role attribute with the value dialog is used.

<div role="dialog">

25. Mark the main header and footer as follows:

<header role="banner">
<footer role="contentinfo">

26. If an element does not have a visible label, it can be denoted using the aria-label attribute.

<svg aria-label="Facebook">

27. It is also very simple to hide decorative elements, for example, icons, from the screen reader:

<svg aria-hidden="true">

28. ARIA attributes can be combined with CSS pseudo-elements to create wonderful accessible components like this slider.

29. Using ready-made libraries of components, resist the temptation to remove all semantic attributes. Accessibility of the application should be for the developer in the first place.

30. WAI-ARIA takes on even greater significance if the HTML markup does not use semantic elements. In this case, only attributes can make the page accessible.

31. Accessibility is gradually becoming the recognized standard of frontend development. In many countries, it is fixed at the level of legislation. Therefore, do not stay away from progress, learn the best practices of creating accessible sites.


32. Many HTML elements have default styles. For example, display: block at div or display: inline at span. There is no need to explicitly specify such properties in your CSS code.

33. Use CSS variables to store base values. This avoids the use of “magic numbers” and makes your code clearer and easier to maintain.

$space-xxs : .4rem;
$space-xs : .8rem;
$space-sm : 1.6 rem;
$space : 2.4 rem;
$space-md : 3.2 rem;
$space-lg : 4.8 rem;

34. It is especially useful to put into variable values of z-index, with which you can otherwise get confused very well:

$zindex-default : 1;
$zindex-footer : 10;
$zindex-header : 30;
$zindex-overlay : 40;


35. CSS combines remarkably well with various HTML meaning attributes, which makes it possible to distinguish semantically meaningful elements.

36. For example, you can stylize elements with different default text language (the lang attribute can be used with any tag, not just with html).

.description[lang="en"] { color: red };
.description[lang="ru-ru"] { color: blue };

37. Or elements not available for interaction:

[disabled] { cursor: not-allowed; }

38. Do not forget that a class is also an attribute, which means that you can use the following selector:


39. CSS allows you to select classes (or any other attributes) whose value contains some combination of characters:


40. Or even one of several combinations, separated by spaces:

span[class~="icon arrow-right"]

41. You can select elements whose attribute value starts with a specific word, for example, icons:


42. Or secure links:


43. Using ARIA attributes and CSS attribute selectors, you can create great things, for example, here are the available tooltips:


44. Pseudo-classes and pseudo-elements of CSS offer tremendous possibilities for styling a document.

45. For example, you can hide empty elements:

div:empty { display: none; }

46. Very often there is the task of discarding some styles from the last element in the group, for example, frames or indents. Previously, this element had to add a special class:

<li class="last"></li>

  li { border: solid black 1px; }
  .last { border: 0; }

47. Pseudo-classes greatly simplify this task:

:last-child {  border: 0; }

48. And there is a better solution!

li:not(:last-child} { border: solid black 1px; }

49. Some interesting and useful examples of using the selector :not you can find in internet.

50. Pseudo-classes solve more nontrivial problems. They can, for example, gracefully select elements that are the last descendant of the parent, but not the only one:


51. The pseudo-elements :before and :after allow you to make as many as three of one HTML element: one source parent block and two of its pseudo-shots. This opens up a lot of possibilities for page styling.

52. Do not forget that these pseudo-elements do not work without the content property, even if it is empty.

:before { content: "Over?"; }
:after { content: "Not yet"; }
:after { content: ''; }

53. Please note that any contents :before and :after cannot have a semantic meaning and is not available for screen readers. Use them exclusively for decorative purposes.

54. By combining pseudo-elements and relative positioning, you can adjust the aspect ratio of the block, which is especially useful when working with images and media objects.

Selector combinations

55. In CSS, you can select elements based on their environment, for example, by selectors of the previous neighbor: + .btn { margin-left: 20px; }

56. Or parent: ~ .btn { margin-top: 20px; }

57. Feel free to combine selectors of different types:

input[type="checkbox"]:checked ~ p { color: red; }

58. You can create completely crazy combinations. For example, this one will select all headers with the class .title (except for the headers of the second level) that lie inside the .main block, which is in the markup after the .header block, which does not have the class .active. Dizzy!

.header:not(.active) + .main .title:not(h2)

59. Using the properties of radio buttons and checkboxes, you can make interactive elements that will appear and disappear when you click:

p { display: none; }
input[type="checkbox"]:checked ~ p { display: block; }

CSS organization

60. Carefully analyze your code for anything that can be reused, and make it components.

61. When developing your own extensible component system, follow the proven best practices of frontend development.

62. Avoid common mistakes in working with cascading style sheets.

63. Use common class naming conventions, such as BEM, to keep your CSS clean and tidy.

64. But remember that BEM is not a panacea, and in some cases it is better to do without it.

65. Use CSS Modules and CSS-in-JS to insulate the styles of different modules. These approaches allow you to no longer suffer with the names of classes. Instead of .header__title, you can write with a clear conscience just .title.

66. If you are not familiar with modular concepts, take a look here.

67. However, there are also drawbacks, the main one being the complication of the development process. So think carefully before introducing CSS modules into your project.

68. CSS-in-JS will be especially useful if you use component JavaScript tools like React or Vue.

69. If you work in a team, be sure to discuss with your colleagues (designers, frontend and backend developers) how to organize the CSS code and component base. This will make it easier for you to select and implement the appropriate tools for your project.

CSS optimization

70. Different selectors operate at different speeds. Consider this by optimizing your code.

71. The fastest – selectors by identifier (#header). Further – in descending order of effectiveness:

  • classes – .header
  • tags – header
  • neighboring elements – nav + header
  • immediate parent – main > h1
  • simple nesting – main header
  • universal selector – *
  • attributes – [type = "text"]
  • pseudo selectors – .header: before, .header: after

72. Avoid large nesting of elements:

body header ul li

73. It is better to mark the desired item using the class than to increase the number of search levels.

74. This article will help you understand the CSS optimization facts and myths.


75. With the advent of Flexbox and CSS Grid, you can stop using float to create layouts.

76. CSS Grid to some extent can replace ready-made grid systems, for example, Bootstrap.

77. No need to choose between CSS Grid and Flexbox, just use them together depending on the tasks.

78. Flexbox is ideally suited for organizing internal one-dimensional marking of containers, for example, aligning the logo on the left, and the menu on the right in the header of the site.

.header {
  align-items: center;
  display: flex;
  justify-content: space-between;
  height: 60px;

79. CSS Grid is a great choice for building complex multidimensional meshes.

Relative Units

80. Assess the advantage of relative units of measure and begin to actively use them in your frontend-development. For example, rem is based on the font size of the root element. If the html property is set to font-size: 10px, then 1rem will be equal to 10px, and 2rem20px.

81. By default, the size of the root element (html) is most often equal to 16px, that is, 1rem = 16px.

82. rem makes it easy to scale all sizes. To make changes, simply increase or decrease the font-size value of the root element.

media (max-width: 600px) {
  html {
    font-size: 10px;

83. Experiment with the viewport units and the calc function to set up a perfectly responsive font size:

html {
  font-size: calc(#{$minimum-size}px + (#{$maximum-size} — #{$minimum-size}) * ((100vw — #{$minimum-viewport}px) / (#{$maximum-viewport} — #{$minimum-viewport})));

Media queries

84. Adapting your website for different screens and devices, use the mobile first approach popular in frontend-development: start layout with a small resolution and gradually increase it.

85. Within a single media query, you can combine all the styles associated with it.

86. A lot of useful information about various media queries can be found in the documentation.


87. Check whether there is a value in the variable, you can use double negation:

const xis = 'x'
const hasXis = !!xis
console.log(hasXis) // true

88. To avoid mistakes, use a strict equality test with regard to the type:

console.log(1 === '1') // false

89. On the other hand, lax validation makes JavaScript programming more flexible:

console.log(1 == '1') // true

90. Variables declared with const are not so constant. You cannot change the variable itself, but its properties are easy:

const animals = ['cat', 'dog']
console.log(animals) // ['cat', 'dog', 'mouse']

91. To make a const variable completely immutable, use the Object.freeze method:

const d2k = Object.freeze(['javascript', 'ninja', 'react])
d2k.push('jquery') // Uncaught TypeError: Cannot add property 3, object is not extensible

92. Use patterned strings for convenient concatenation:

const f = (s = '❤️') => `I ${s} frontend`
// I ❤️ frontend

93. Spread-operator makes it easy to combine arrays:

const skills1 = [ 'css', 'html' ]
const skills2 = [ 'js', 'ts' ]
const newSkills = [ …skills1, …skills2, 'ux' ]
console.log(newSkills) // [ 'css', 'html', 'js', 'ts', 'ux' ]

94. And objects:

const obj1 = { a: 1, x: 'Hello' }
const obj2 = { b: 2, y: 'world' }
const mergedObj = { ...obj1, obj2 }
console.log(mergedObj) // { a: 1, b: 2, x: 'Hello', y: 'world' }

95. And turn a string into an array:

const s = 'HTML'
const c = [ …s ]
console.log(c) // [ 'H', 'T', 'M', 'L' ]

96. With the help of destructuring assignment, you can easily write object properties into variables:

const person = { name: 'Felipe', lastName: 'Fialho', age: 28 }
const { name, age } = person
console.log(name, age) // Felipe 28

97. The remaining properties are not lost anywhere, they can be turned into a new object using the rest-operator

const person = { name: 'Felipe', lastName: 'Fialho', age: 28 }
const { name, …restOfPerson } = person
console.log(restOfPerson) // { lastName: "Fialho", age: 28 }


Use the selected resources of the frontend developer:

98. MDN Web Docs – the best web documentation.

99. CanIUse – reliable verification of support for various features by browsers.

100. W3C standards – first-hand frontend standards.

Hiding JavaScript code on the front-end from outsiders

Let’s imagine a situation where you and your team write interesting and complex JavaScript code. Moreover, this code should be used in the project as soon as possible. If its functionality is truly unique, then during the development process, both you and the team members quite reasonably have the question: “How to protect the code from being copied?”.

© Designed by Vecteezy

How to protect the code: web sockets, cryptors and obfuscation

Of course, we all understand perfectly well that JavaScript runs on the browser side. And any encrypted code can always be decrypted, if you know the principles of the browser. Therefore, you can only try to make it difficult to understand this code, and this, in turn, will greatly prevent an attacker from modifying it to fit his needs.

So, there are several options for protecting the code:

  1. Use web sockets.
  2. Use cryptors.
  3. Obfuscate the code.

Cryptors bring the code into an unreadable form, using, as a rule, base64 (which inevitably leads to an increase in the amount of code by about 30%). Then, the so-called “salt” is added to the result, a set of characters that is used as a key when the code is interpreted by the decoder function. Well, then the entire line of code is usually executed via eval(). The problem of cryptors is that if you understand the principle of their work, cut off the “salt” and decode, then immediately all the code becomes available in its original form.

Obfuscators, on the other hand, change the code itself by inserting unreadable characters between operators, changing the names of variables and functions to a set of visually incomprehensible characters. At the same time, the amount of code is also greatly increased due to the insertion of additional pseudo-code, as well as the replacement of characters with hex, when any characters are converted to their hex values (for example, the Latin letter 'e' can be written as '\ x65', and this is fine interpreted by any browser). You can see how the translation to hex works through any Text To Hex service, for example on Crypt Online.

The use of obfuscators greatly complicates further debugging of the code, since this is an irreversible process. In addition, in some cases they may affect the functionality of the code. You can try obfuscators on any obfuscation service, for example this or that. Also on the web, you can find paid obfuscator cryptors, in the settings of which you will be able to specify the degree of protection, the lifetime of the script, etc. In this case, the script will be tightly bound to your domain, i.e. unique value for your host will be used for decryption. The cost of such cryptors starts from $45. In addition, before obfuscation, you can pre-minimize the code by replacing all the names of variables and functions with their single-character synonyms. A great and very popular tool on Node.js is UglifyJS, which works both in automatic mode (say, through Gulp) and in command line mode.

Go to UglifyJS

There is also the well-known Closure Compiler from Google, which, apart from minimization, analyzes JavaScript code, deletes dead code, rewrites and minimizes what is left. It also checks syntax, variable and type references, and warns about common JavaScript errors. It has a well documented API.

In addition to the proposed methods, you can do the following:

  • use WebStorage and hide JavaScript code there;
  • hide part of the code in a separate file on the server and call it via XMLHttpRequest;
  • use bitwise operators to replace numbers with sets of brackets and signs ~;
  • substitute standard JavaScript functions and methods.

All this, of course, will not be one hundred percent protection. Nevertheless, the more complicated the decryption process, the greater the chances that after many unsuccessful attempts, fans to copy someone else’s code will leave your site alone.

Encryption of code on the example of a JavaScript calculator

Not so long ago, I developed a JavaScript calculator for calculating the cost of services, with a large number of interrelated parameters. The management set the task of protecting this script from being copied so that competitors could not use it on their websites. I searched for various solutions, found nothing suitable, so I started writing my own. I present it below.

I draw your attention to the fact that any code can be decrypted, just for this it takes time. Therefore, this solution is, of course, not perfect. However, it takes time, attentiveness and perseverance to uncover it. And this may alienate your competitors from the idea of ​​copying your script. After several unsuccessful attempts, most of them will simply look for an analogue of a similar script on other resources.

As a result of the work in the browser, you will see something like this:


At the same time, all encrypted scripts will work correctly. Experienced view of the programmer will immediately visually determine the encoding via base64. But when trying to decode a string with any base64 decoder, there will be an error. If you insert the script in the alert (this method is also recommended on the forums to decrypt the code), then the result will also be zero.

At the same time, no one knows that the script is encrypted here. This may be some parameter, text or image. Through base64 you can encrypt anything.

Let’s look in the code for the function glob(), which is passed an encrypted string. Here it is: glob=function(s){sfd(rty(s.substring(-~[])));

We see some more functions of sfd() and rty(). We are looking for these features. Here they are:


At this point, many will finish the decryption attempt and leave your site alone.

Consider the algorithm in more detail.

How to protect JavaScript from copying on your site

First of all, specify the path to the script in the footer of the site and immediately encode it:

<?$filebase64='K'.base64_encode(file_get_contents('<script file path>'));?>

In the line above, we tell the PHP interpreter to take the file script.js, then encode it through base64, add the line ‘K’ and write all this into the $filebase64 variable.

Adding the string ‘K’ (it can be any Latin letter or a combination of letters or numbers) protects us from wanting to copy your script to decrypt it using alert() or an online decoder. After all, with additional characters, the script will not work.

Then somewhere in the code we call the script:


Let this script be called separately, away from other scripts and links to scripts.

Next, somewhere in the file with common site scripts, separately from other scripts, insert the call to the decryption functions. You can insert independently of other functions and libraries.


We analyze in detail what is happening here.

Our main function glob() takes one parameter s. It is immediately passed to the substring() function with the -~[] parameter (this is 1 in encrypted form), which extracts the string from s from the first character to the end. Therefore, if we in the PHP code add more than one character as a string, say three, then we will need to specify 2+(-~[]) in the substring() function. Or, by encrypting digits through bitwise operators, we can create a confusing formula, some of the variables of which we can hide in cookies or sessionStorage, which will make it extremely difficult to figure out how many characters need to be dropped to decrypt the code.

An example of replacing numbers with a bitwise operator ~:

  • -1 can be replaced by ~[]
  • 1 can be replaced by -~[]
  • 0 can be replaced by ~~[]

Next, the result is taken by the rty() function. This function is a set of characters, in particular: this["\x61\x74\x6F\x62"];

Try typing this into a browser console and you will see what this feature actually does. For example, you will see:

ƒ atob() { [native code] }

That is, the character set is an encrypted atob() function, which, as described in MDN, decodes a string encoded using base64.

The decoding result is obtained by the sfd() function. It is also a set of characters: this ["\x65\x76\x61\x6C"];.

Have you guessed what to do? Run in the browser console and you will see:

ƒ eval() { [native code] }

Here, I think, it is not necessary to explain anything. We all know that the eval() function executes the script obtained from the string. “Bad practice”, as many would say, for regular code, but in our case it is a safe and necessary function for implementing our idea. In addition, the user cannot directly access this function.

You probably wondered how, in the same way, are the functions encrypted in the character set? Very simple: a character set is text converted to hexadecimal notation. That is, it is a hex (hexadecimal) text in which you can encrypt any characters.

Thus, our decoded function looks like this (it’s specially broken down into lines so that it is visual):

glob = function(s) {

As a result, we discard the first character of the encrypted string (at the same time there may be at least 353 characters, and no one can quickly guess about this), then decrypt it, then execute it with eval().

You can go further. If in some way, someone still decrypts your script, complicate it a bit to make it harder for people to modify it. For example, the bitwise operator ^, which works great. For example, a^b^b will be equal to a. As b, a key can be used, which we encrypt somewhere above.

Everything will work as before, but it will be confusing for bad lovers to copy your code.

By Denis Lisogorskii

How to replace all identical substrings in a string with JavaScript

This time, let’s figure out how to replace all the same substrings in a particular string. Suppose we have a line:

var str = "Test abc test test abc test test test abc test test abc";

And our code should replace, for example, all the abc substrings in this line. Using the replace method does not work, because in this way we will replace only the very first substring abc.

There are at least two optimal solutions to this problem: the use of regular expressions and the methods split and join.

Attention! In the examples below, we will solve this problem by rewriting the methods built into JavaScript. This is not the best practice and is strongly not recommended for use in real projects. We will use it exclusively for demonstration purposes.

Method One – Regular Expressions

String.prototype.replaceAll = function(search, replacement) {
    var target = this;
    return target.replace(new RegExp(search, 'g'), replacement);

Method two – split and join

String.prototype.replaceAll = function(search, replacement) {
    var target = this;
    return target.split(search).join(replacement);

As for performance, it is worth noting that the first solution works as much as twice as fast as the second.

However, there is a snag in the solution using regular expressions: it is assumed that the user will not use the characters reserved for regular expressions as the first parameter. Users are different and often do not do the most reasonable things with our creations, so we will not risk and prevent this scenario.

We need a function that will format the search argument:

function escapeRegExp(str) {
  return str.replace(/[.*+?^${}()|[\]\]/g, "\$&");

All that remains is to call the escapeRegExp function in our String.prototype.replaceAll:

String.prototype.replaceAll = function(search, replacement) {
    search = escapeRegExp(search);
    var target = this;
    return target.replace(new RegExp(search, 'g'), replacement);

Navigation with arrows by Javascript

On many sites, pages can be navigated using the keyboard by pressing the ← and → keys, while holding the Ctrl key.

What this requires is generally understandable. Somewhere in the code you need to hide the addresses of the previous and next pages and “hang up” the handler at the touch of a key.

Where to write the address? In general, anywhere. We use tags <link>, adding IDs to them:

<link rel="next" href="/portfolio/sokol/2005/" id="NextLink" />
<link rel="prev" href="/portfolio/samsung/megaded/" id="PrevLink" />
<script type="text/javascript" src="/svalka/navigate.js"></script>

This choice at the same time solved another problem – some browsers respond to these tags by displaying additional buttons on the navigation bar.

It remains to hang up the handler for the onkeydown event:

document.onkeydown = NavigateThrough;

And wait until one of the arrows is pressed:

function NavigateThrough (event)
	. . .
	switch (event.keyCode ? event.keyCode : event.which ? event.which : null)
		case 0x25:
			link = document.getElementById ('PrevLink');
		case 0x27:
			link = document.getElementById ('NextLink');
	if (link && link.href) document.location = link.href;	

Opera is an exception. This browser transfers control to the event handler after it executes the default action (the Ctrl + arrow combination in it is already taken). Therefore, in Opera it is necessary to press not only Ctrl, but also Shift – such a three-key combination corresponds to navigation through links from <link> tags.

When you do such things, it is important to correctly figure out which page to consider the previous one, and which next.