In this tutorial we will check how to serve an external CSS file, to be included by an HTML file. This will be implemented on the ESP32 running the Arduino core and using the HTTP async web server libraries. Both files will be served by the ESP32.
In terms of code organization, it is much cleaner to separate the HTML and CSS files, since HTML is related with the content and CSS with the appearance. So, in theory, we can work in both independently.
Besides that, if HTML elements in two different files need to use the same style, then we don’t need to duplicate the same style because both HTML files can include the same CSS and reuse classes. You can check more about why we should do this separation here.
Our simple example will consist on a very basic dashboard that will be used to display some temperature and humidity measurements. Besides the basic HTML to render the elements, we will change their style with some CSS, which will also be served by ESP32 and included in the HTML file as an external resource.
The CSS will add some borders, change the colors and give a rounded shape to the HTML elements that will show the measurements.
Note that for this example, the measurements will be hardcoded in the HTML file, but this could be used as the front end for an application that connects to the server and fetches sensor values periodically, like we have covered here.
In order to keep the ESP32 implementation clean and avoid big in memory strings, we will serve both the HTML file and the CSS file from the SPIFFS file system. For a basic example on how to serve content from the file system, please check here.
For this example we will not minify the files, but for a final application you should do it in order to increase the performance of the application and to reduce the size occupied in the ESP32 file system.
To make the process of uploading files to the ESP32 really simple, we will use the Arduino IDE plugin covered here. Although the procedure is detailed in the mentioned post, we basically need to navigate to the Arduino sketch folder and create a new folder there called “data“.
Inside the “data” folder, we should paste both the HTML and CSS files, as shown in figure 1. The code will be analyzed below in the next sections.
Then, to upload the files, assuming the plugin is already installed, we go to to the tools menu of the Arduino IDE and click “ESP32 sketch data upload“.
Figure 1 – Data folder inside Arduino sketch folder.
Taking in consideration the names and extensions of the files shown in figure 1, both should be uploaded to the ESP32 SPIFFS file system to the following full paths: “/dashboard.html” and “/“.
Please note that the focus of this tutorial is not the development of HTML or CSS code. So, the explanation below is not detailed and, for simplicity, we are not following all the best practices.
The tests were performed using a DFRobot’s ESP32 module integrated in a ESP32 development board.
We will start the HTML code by putting a link tag on the head of the document, in order to include an external CSS file. The link tag allows to define a link between a document and an external resource [2]. In our case, the external resource will be the CSS file.
On the rel attribute of the link tag, we need to specify the relation between the HTML document and the linked document [2]. In our case, we use the value “stylesheet“, since we are going to import a CSS file.
Then, in the type attribute, we should specify the media type of the linked resource [3], which will be “text/css”.
Finally, in the href attribute, we need to specify the URL of the external resource [4]. In our case, since we are going to serve the CSS file from the same server that will serve the HTML file, we should use a relative URL. We will simply use the name of the file, which will also need to be the name of the route in the Arduino code.
Then, in the body section, we will have the elements that will compose our simple dashboard. We won’t enter in great detail regarding the HTML code, since our focus is on the end-to-end system and on how to serve the external CSS file.
Nonetheless, as a brief summary, we will implement both measurement displays with some HTML divs. Each display will show in bigger letters the current value for that measurement and, in lower letters below, the type of measurement. Note however that the sizes of the letters will be specified in the CSS styles, so here we will only have the content.
Note that, for the temperature units, we will need to use the “º” character, which may cause some encoding problems when transferring the file to the ESP32 file system with the IDE plugin. In order to avoid that, we are going to escape it to º.
If you experience problems with other characters while developing HTML code that will be transferred to the ESP32 file system, please check this tool in order to get the escaped values, so you can use them instead.
We are specifying the class names of the HTML elements, so the correct CSS styles are applied accordingly to what will be defined in the .css file.
<body> <div class="dashboard"> <div class="circle-container"> <div class="circle"> <div class="circle__content"> 24ºC </div> <div class="circle__lower-text"> Temperature </div> </div> </div> <div class="circle-container"> <div class="circle"> <div class="circle__content"> 22% </div> <div class="circle__lower-text"> Humidity </div> </div> </div> </div> </body>
We will also only take a brief look at the CSS code that we will be used. As an initial note, please take in consideration that it is a common good practice to keep the properties inside classes in alphabetical order. You can read more about it on this link. Also, the CSS classes below follow the BEM methodology, which is the reason behind the “__” on some class names.
We will have a class, called “dashboard“, that will be used to align the whole dashboard horizontally.
The “circle-container” class is just a wrapper to help align both displays of the dashboard and the “circle” class implements most of the styling of the circular displays, which includes the rounded borders and the vertical alignment of the text of the elements inside..
The “circle__content” and “circle__lower-text” classes specify the styling of the display’s main content and lower text.
The full CSS can be checked below. As already mentioned, the objective of this tutorial is not to focus on the CSS, so other alternative and possibly more optimized implementations could have been followed.
.dashboard{ text-align: center; } .circle-container { display: inline-block; margin: 10px; } .circle { border: 5px solid #0074D9; border-radius: 50%; display: table-cell; height: 100px; vertical-align: middle; width: 100px; } .circle__content{ color: gray; font-family: Arial, Helvetica, sans-serif; font-size: 34px; } .circle__lower-text{ color: gray; font-family: Arial, Helvetica, sans-serif; font-size: 11px; }
The first part of the ESP32 Arduino code will correspond to the library includes. We will need the WiFi.h library, so we can connect the ESP32 to a WiFi network, the ESPAsyncWebServer.h to setup the server and the SPIFFS.h, so we can retrieve the files from the file system.
After that, we declare the credentials of the WiFi network as global variables, so they are easy to change. To finalize the global variable declaration, we will need an object of class AsyncWebServer, so we can configure the server routes in the Arduino setup function.
#include "WiFi.h" #include "SPIFFS.h" #include "ESPAsyncWebServer.h" const char* ssid = "yourNetworkName"; const char* password = "yourNetworkPassword"; AsyncWebServer server(80);
Moving on to the Arduino setup, the first thing we will do is opening a serial connection. After that, we will mount the file system and connect the ESP32 to a WiFi network, using the previously declared credentials.
Serial.begin(115200); if(!SPIFFS.begin()){ Serial.println("An Error has occurred while mounting SPIFFS"); return; } WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } Serial.println(WiFi.localIP());
After that, we will configure the server routes. The first one will be serving the HTML file. We will call it “/dashboard” and it will only allow the client to perform HTTP GET requests.
In the route handling function, we will simply serve to the client the HTML file stored in the file system.
We do so by calling the send method on the AsyncWebServerRequest object and passing as first input the SPIFFS variable, as second input the path to the file and as third input the content-type, so the client knows how to interpret the payload returned by the server.
Remember that the file path will be “/dashboard.html“, since it is located on the root folder of the ESP32 SPIFFS file system and it keeps the original name it had on the computer from where the upload was performed.
The content-type will be “text/html“, since we are serving a HTML page.
server.on("/dashboard", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/dashboard.html", "text/html"); });
We will need an additional route that will be responsible for serving the CSS file, which will be included by the HTML page to apply the styles to the elements.
The route name should match the name of the .css file we will be serving (remember that we used the filename on the HTML href attribute). So, the route will be called “/dashboard.css“.
Again, the route will only listen to HTTP GET requests, since no other method makes sense when fetching a file.
In the handling function implementation, we will return back to the client the CSS file from the file system. We use again the send method of the AsyncWebServerRequest object, passing as first input the SPIFFS variable, as second the file path (“/dashboard.css“) and as third the content-type. Note that since this file only contains CSS, the content-type should be “text/css“.
server.on("/dashboard.css", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/dashboard.css", "text/css"); });
To finalize the Arduino setup function, we still need to call the begin method on our AsyncWebServer global object. Only after that the web server will start listening to incoming HTTP requests.
The final source code can be seen below and already contains this method call.
#include "WiFi.h" #include "SPIFFS.h" #include "ESPAsyncWebServer.h" const char* ssid = "yourNetworkName"; const char* password = "yourNetworkPassword"; AsyncWebServer server(80); void setup(){ Serial.begin(115200); if(!SPIFFS.begin()){ Serial.println("An Error has occurred while mounting SPIFFS"); return; } WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } Serial.println(WiFi.localIP()); server.on("/dashboard", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/dashboard.html", "text/html"); }); server.on("/dashboard.css", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/dashboard.css", "text/css"); }); server.begin(); } void loop(){}
To test the code, simply compile it and upload it to your ESP32 device using the Arduino IDE, with support for the Arduino core installed. Once the procedure finishes, open the IDE serial monitor and wait for the WiFi connection procedure to finish.
Once the device is connected to the WiFi network, copy the IP address that is printed to the serial monitor. Then, open a web browser of your choice and type the following URL, changing #yourDeviceIp# by the IP you have just copied.
http://#yourDeviceIp#/dashboard
You should get an output similar to figure 2, which shows the HTML web page with the CSS correctly applied to all the elements. You can also check in the developer’s console that the request for the .css file is indeed performed and answered with success.
Figure 2 – HTML page rendered correctly with CSS.