The Making (and Potential Benefits) of a CSS Font

Avatar of Levi Szekeres
Levi Szekeres on

DigitalOcean provides cloud products for every stage of your journey. Get started with $200 in free credit!

Not a typical one, at least. Each character is an HTML element, built with CSS. A true web font!

Let me elaborate. This is a way to render text without using any font at all. Random text is split with PHP into words and letters, then rendered as HTML elements with classes. Every element is styled with CSS to create the characters. This is “just” HTML controlled with CSS, but still, it is software and it gets the message through. It has all the properties a conventional font does, so we’ll call it a font. A font without a format.

Disclaimer: I’m not an expert in HTML, CSS, or PHP. I’m willing to bet there is a shortcut or an easier solution to achieve what I’ve done here, but since I’m happy with the results, I will present the process and my experience. The presentation is not a tutorial; it is an experiment based on my limited skills and should be treated as such.

The idea

The project was never meant to last for five months, but that’s what it took! It all started with having a play with a CSS icon, using pseudo-elements to make shapes. Once the first S letter was finished, the rest were relatively easy. I checked to see if there were other similar projects but didn’t find much, so I was motivated to see how far I could get.

Initially, an SVG font controlled with CSS seemed like a good idea. It would make this task much easier (SVG is made for drawing) and could focus on design-specific effects, but it doesn’t have the flexibility of a raw HTML element. An SVG cannot be modified depending on context, and the process falls back to the conventional font design, where every character has a fixed shape and code.

How it works

This is a hybrid of web and font design. Each character is built like any web element and used inline to behave like a font. Metrics, Weights, OpenType Features and all the other font properties are controlled exclusively with the CSS file.

The font design is based on the border width of the elements, which makes it extremely versatile. With the exemption of script fonts, several styles and weights can result just from border variations, using the same shape. On more complex characters the clip-path and the background is used to create the cutout effect.

Nested elements are generated when the ::before and ::after pseudo-element is not enough to form a character. Using em values for width, height and border widths will help later in controlling the font size. This is one of the golden rules.

A character (left) is built like any CSS icon (right). There are no major differences. Sometimes a letter is easier to build, just like a stickman, based on circles and lines. But here is when you can really appreciate the role of the border-radius property. Personally, I never was a fan of rounded borders, but this experience changed my mind. Basically, there’s no limit for what a radius can do.

Below are the only two “real” examples of the CSS font in this article, the rest of the example figures are converted to SVG for easier display in a blog post.

1. Gray – main shape
2. Red – pseudo elements
3. Blue – Extra element
Most of the times you have to insist for the desired effect, but in the end you want to carry this icon in your wallet. Icon created with a single shape, the pseudo-element is used only to avoid the rotation of the main element, in case it is used inline.

The serif preview presents a more complex situation, but as usual, a sans font will have fewer elements to deal with, making the file is smaller and load faster. This is not really an issue and it’s only logical — the CSS is read before a font embedded with the @font-face rule.

The challenge

Naive start, putting all the math and the logic to use. None of the values work if a pixel decides so. The final solution in this case was to include a second parent that will keep the “children” tidy.

The hardest part is to beat the pixel ratio, or align the pseudo elements to the base shape. Elaborated mathematical formulas failed when the charater was resized. A browser will treat each element separately and shift them to the closest integer value.

A solution to this was to create as many pseudo-elements as possible (even including extra elements), and use a single reference for a pair of ::before and ::after, not related to the main shape. In this case, the browser will render the elements more or less to the same position.

A character without a point of reference is illustrated with the S letter below. The top and bottom section of the letter are two pseudo elements, without a base shape to rely on (e.g. the gray area in the serif above or here in the digit two).

After creating a few hundred characters, you realize that a character cannot support inline transformation (i.e. skew(), rotate(), and such) because it won’t align to siblings. This becomes more obvious visually on text selection. Because of that, a pseudo-element makes perfect sense. I would say essential: the second golden rule.

The graphic shows the basic principle of construction. When apertures cannot be controlled simply with the {border: 0;} rule, clip-path is used.
In this case, the S letter needs a smaller aperture, so the border eventually is kept and cut at the desired distance with clip-path.

CSS custom properties

It seems easier to create a style in CSS than it is in font softwares. You have the option to control shapes and sizes for multiple characters at once. In CSS, more characters are grouped together in the same ruleset.

CSS custom properties are extremely handy in this situation, especially for controlling borders, widths, and positions. The different weights are the result of changes in the variable, with adjustments afterwards. Fine tuning is unavoidable because character shapes and sizes take the border width into account and may not display proportionally with different borders, especially on asymmetrical shapes.

Cutout effect created by adding the same background color to the overlaying element. Combination of colors and effects using the mix-blend-mode.

The cutout effect is created by adding the same background color to the overlaying element, then using a combination of colors and effects using mix-blend-mode.

A global color variable is required in CSS to create a cutout effect for nested elements that otherwise would follow the parent color (overlaying elements match the background).

The background-image property won’t work on characters built exclusively with borders and the background is changed if the element has size or position transformations (scale, rotate, or other).

Where a background cannot be used, the solution is mix-blend-mode: lighten; on dark backgrounds and mix-blend-mode: darken; on light backgrounds.

Side effects

The downside is that some effects can have unexpected or even opposite results on elements with variable properties. Usually, a filter will read elements as full objects. To prevent any conflict, borders and transformation effects are reserved for the font design.

Font to text

A font won’t make a text. The idea in the first place was to create a text that will load along with the CSS, without any dependencies. For that the best option is PHP (my rookie opinion). Besides rendering HTML with inline functions, it is up to almost any task imaginable. Without PHP this project would not be possible.

Naturally, the first task with PHP was to split a random text, remove extra spaces and create matching groups for every word and letter, each one with its own class. So far, so good. I won’t insist on the part that went smoothly, it is a basic function, using split, explode and all the other words borrowed from a video game.

Still, since I never worked on this before, I had to learn the hard way. Nobody told me that PHP considers the “0” (zero) as null, so there’s a day gone. I couldn’t figure out why my zeros are not displayed.

For anyone with this issue maybe it’s helpful. Instead of using the empty() function, I used the one below:

function is_blank( $value ) {  
  return empty( $value ) && !is_numeric( $value );

The other major issue was the character range. It seems that there are way too many settings in HTML, the .htaccess file, and on the server itself just to recognize special characters. The solution was found after a few days in the PHP Documentation, posted by qeremy [atta] gmail [dotta] com, obviously somebody living in a diacritic-heavy area.

function str_split_unicode( $str, $length = 1 ) {
  $tmp = preg_split( '~~u', $str, -1, PREG_SPLIT_NO_EMPTY );
  if ( $length > 1 ) {
    $chunks = array_chunk( $tmp, $length );
    foreach ( $chunks as $i => $chunk ) {
        $chunks[$i] = join( '', ( array ) $chunk );
    $tmp = $chunks;
  return $tmp;

A lot of chunks, if you ask me, but it works like a charm and solves every issue. The function basically overlooks the language settings and will read any character, even the non-standard ones. Characters buried deep in the Unicode tables will be recognized if the PHP function includes that character.

This function will only create the possibility to generate each character as typed, without the need for HTML entities. This option won’t limit the use of text in HTML format, but inline codes must be avoided or replaced with alternatives. For example, instead of using non-breaking spaces (&nbsp;), elements can be wrapped in the <nobr> tag.

The HTML entities are not decoded, which is an advantage from my point of view.
Default system font on the left. On the right is CSS text rendered without any changes.


With this solved, the next step is to create a specific structure for each character. The class of the HTML elements and the positions of the nested element depends on a long list of characters that correspond with one or more classes. Some of the most basic characters are not excluded from this list (e.g. the small “a” letter needs a finial and that means an extra element/class).

The basic structure looks something like this, just to get the idea …

'Ć' => 'Cacute C acute'

…which will render three elements: the parent Cacute, the C letter, and the acute accent.The result is below, where the red square represents the parent element, containing the other two preset elements.

Cacute — a classic combination between a basic letter and an accent.

The technique is very similar to the way diacritics (sometimes ligatures) are built in font software, based on pairings. When a component element is changed, every other will adjust.

Because any element can have multiple applications, the IDs are avoided and only classes are used.

OpenType features

The PHP function is set to behave differently depending on context. The character recognition is set to replace pairings and create ligatures when rendering the CSS text.

Contextual ligatures in the CSS text are not standalone characters and don’t have specific classes. Different from conventional OpenType features, the characters are restyled, not replaced. The interaction is controlled in CSS by styling the second element, to merge or form a new character.

The features are activated with a specific class added to the parent container. Alternates are rendered in any circumstance, regardless if a character is registered or not, in every browser, with or without font feature support.

Example of the Ordinal Indicator Feature, activated with the .ordn class. The characters are styled inline and change their look according to class and their previous sibling. The same recipe is applied for Stylistic Alternates (salt), Oldstyle Figures (onum), Slashed Zeros (slsh), Superscripts (sups), Subscripts (subs) and Fractions (frac).
Classes with similar functions to OpenType are named after the Registered Features by Microsoft/Adobe.

HTML syntax

Any HTML element can include the CSS font, as long it has the .css class next to the weight of the font. To select a weight, the .thin, .light, .regular or .bold class is used, something like <pre class="regular css"> (the <pre> tag is just a safety measure to avoid any style interference).

Available weights: Thin, Light, Regular and Bold.

The text can have an HTML format. The plain text is not mandatory.

PHP will ignore a bracket (<) if this has a closing correspondent, which means that every HTML tag in a text will remain active and only the text content is rendered as the CSS font. URLs, file paths, or other additional info found in the tag are encoded just the same by the browser. The same tag can style groups of letters or entire sentences, if they’re set in CSS.

Also, depending on layout preferences, specific tags — like <a>, <u>, <ins>, and <del> — can be treated as objects to emulate and customize their native appearance and behavior.


CSS text is a group of objects with borders, open for size and color treatments. Think color as border-color and vice-versa. :first-child instead of :first-letter.

The font-size is set in the CSS file, the same as any other font, using viewport, percentage, pixels, em or rem units. Values set in pixels work with decimal values.

The text-align and text-indent properties work by default. The text will align to any setup even without text content.

Block-level elements (e.g. <div>, <p>, <ol>) placed inside texts will cause a line break, as it would normally. The <br> tag works as expected.

Except for text formatting elements (e.g. <h1><h6>, <strong>, <em>, <small>, <sup>, <sub>, etc.) that will need new rules to have the right effect on the text, most of the semantic elements (e.g. <form>, <ol>, <li>) work with their custom settings.

The font

To test the font in dynamic content, part of the PHP function was reproduced in JavaScript, with paste, mouse events, caret positions, and text selection. A single keystroke now makes it all worthwhile.

The CSS font and the complementary icons. This is what actually started the whole thing!

Review! Pluses (+) vs. Minuses (-)

Instant load

In the absence of actual text, the browser doesn’t wait for a font and a script to render the page. The CSS file along with HTML elements are cached, which means faster loads.


Every browser and server recognizes CSS. Fewer worries to find the right format that works the same in every browser. A server will not check for a specific format to allow access.

No dependencies

The CSS font doesn’t need alternate or system fonts to display the text. The same CSS that styles the page can include the font. The browser will not display a default font, neither before nor after the page load. The font does not rely on third parties and scripts, and the design is not different on browsers with disabled scripts.

No embedding

The CSS font is fully integrated into a webpage, and adapts to the layout without replacing other elements on load. Every page property is automatically valid for the text and this will show up the way it was intended, without after effects or functional issues.

Selective use

The font can be reduced to a limited number of characters. The full version is not required if the layout has a single word or a symbol, for example.

Full security

The actual text is not present on the page, which means that sensitive informations can be easily displayed without the fear of spam or phishing.

SEO friendly

Important information can be included using tag properties, the same way the alt attribute works for images.


To build complex characters or functions, the font is open for any HTML element. No need for scripts to get specific details because every word and letter has its own entity and can be styled individually.


The font design is not limited to predefined characters, so the style can change depending on context, without creating new characters.


To compensate for the lack of automation found in font softwares, in CSS, the design can control several elements at once. This argument is valid, since a font software works with existing content, while CSS works with properties, creating a template for every existing or future elements.


Anyone can create their own font. Short texts can be rendered manually, and the PHP function is not a requirement.


The design is accessible with any text editor or developer tool. Elementary skills using border widths, border radius, shapes and sizes is enough to redesign any character.


Every adjustment result is instant. Conversions, exports, uploads or other steps to activate the font are eliminated from the process.

Moderate use

The speed of the page may suffer if a CSS font is generated for extended texts. This technique is only recommended for headlines, titles, excerpts and short paragraphs for that reason.


The CSS font will not benefit from special treatments because, to the browser, this is just another HTML element. As a result, there’s no optimization or kerning support. The pixels have a hard time sharing thin lines and at small sizes the font may display improperly.

Hard coded

Your usual font settings are unavailable by default and styling tags (e.g. <strong>, <em>, etc.) have no effect. The functions must be set in the CSS file and require a different approach, working with HTML elements instead of fonts.


This is a webfont, so it is limited to digital media controlled with CSS. Except for some bitmap effects, the font can only be translated for offline by printing the document as a PDF, which will convert the CSS into a vector format.


Without a standalone file the font is hard to be identified, tested, or transferred. It works like the HTML color: it’s invisible until it’s generated.

Not selectable

Without extra scripts, the text cannot be selected or used in inputs and textareas. For dynamic content, the function needs the whole character recognition found in PHP.

Not interactive

The most common display functions, such as sort or filter, will have to work with classes, and not with text content.

Not printable

The online print supports only basic CSS rules, sometimes ignoring graphics in favor of texts. The print quality will rely strictly on the browser’s capabilities.

No accessibility

The CSS font will adjust to page zoom, but the font size and the languages cannot be changed through the browser.
Custom browser functions (e.g. Find, Reader) cannot access the text content since there is none.

Limited design

There is no wide selection of styles to choose from and the design is limited to the capabilities of CSS. A CSS rule can have different meanings to different browsers, causing inconsistencies. A CSS font is written, not drawn, so the “hand-made” concept is eliminated completely.


You need to know your CSS to make adjustments in the font, and vice versa. The design process is not automated, and some properties that are otherwise generated by a machine must be set manually.

No protection

The design code is accessible to anyone, the same as any online element. The design cannot be really protected from unauthorized copying and usage.

Thanks for reading! Here’s the fonts homepage.