One place for hosting & domains

      blog

      How to Create Documentation for Your REST API with Insomnia


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      In this tutorial, you will document your API using the OpenAPI specification (v3). An OpenAPI file is a JSON or YAML file that follows the OpenAPI specification. This specification defines what fields your JSON/YAML file must contain and how it will be reflected on the documentation service you’ll use to host it. Many services support OpenAPI, so you can pick and choose, or even use multiple services, without having to change your API documentation’s format.

      To create the documentation, you’ll use Insomnia, a free and open-source application that allows you to test your API and design the documentation with a real-time side-by-side preview. Insomnia doesn’t support JSON, but it does make it easy to write YAML. YAML is a good choice for API documentation because these documents can get very large, and a JSON document would get cluttered and hard to read.

      Finally, you’ll host the API documentation with Redoc, an open-source application used by many companies. Redoc takes the OpenAPI document you generated and gives you an HTML page that displays a nice-looking and interactive version of your documentation. You’ll also deploy your Redoc generated site to GitHub Pages, which is a free website hosting solution by GitHub.

      In this tutorial, you will learn more about OpenAPI, document your API according to the OpenAPI Spec in Insomnia, and host this documentation on GitHub Pages with Redoc.

      Prerequisites

      To follow this tutorial, you will need:

      Step 1 — Understanding Your API

      In this step, you’ll note the routes your API accepts and their relevant parameters and responses. Since you’ll be documenting the API for others, and because you may also refer back to this documentation in the future, it’s important to note everything you need to document. OpenAPI allows you to define request bodies, headers, cookies, and even possible responses for each API route.

      This tutorial will use the JSON Placeholder API, which is a free mock API. Since this API is quite large, you will only be documenting the /posts section in this tutorial.

      The below table shows the method, route path, and description of each of the five routes you will document in this tutorial. Making a table or something similar is helpful so that you don’t forget any route (which can happen if the API is big).

      Method Route Description
      GET /posts Get all posts
      GET /posts/:id Get a single post
      POST /posts Create a post
      PUT/PATCH /posts/:id Update a post
      DELETE /posts/:id Delete a post

      Now that you know what your API can do, it’s time to begin documenting it with Insomnia.

      Step 2 — Creating an Insomnia Project

      In this step, you’ll create an Insomnia project. An Insomnia project contains the OpenAPI document, any tests you write for your API, and any requests you’ve created. The interface is split into three tabs: Design, Test, and Debug. You’ll focus on the design tab for this tutorial.

      Open the Insomnia app and go to your dashboard. Create a new Design Document by clicking the Create button on the top right of the Insomnia window and give it a name. For this tutorial, you can use json-placeholder-docs.yaml.

      Note: YAML by design only accepts spaces as indentation. Insomnia, however, indents with tabs by default. This will be fixed in a later update, but for now, open your Preferences by clicking the cogwheel icon on the top right, or by pressing Ctrl/Cmd + ,. In the Font section, uncheck Indent with Tabs and close the Preferences window. This will make Insomnia use spaces instead of tabs.

      You should now see three panes, as shown in the following screenshot below. The first pane shows an overview of your document, such as the routes of your API and components you’ve defined (you’ll learn more about those later). The middle pane contains the code editor that you’ll use to write the OpenAPI document in YAML. This editor also detects errors automatically and notifies you of them at the bottom. Finally, the last pane on the right is a real-time preview of the document. You’ll see an error because you still have to tell Insomnia which version of OpenAPI you’ll be using.

      Screenshot of Insomnia showing three panes. The first two panes are blank. The third pane on the right shows an error:

      In the code editor of the Design tab, add the line: openapi: 3.0.3. This indicates the version of the OpenAPI spec you will be using. At the time of writing, the latest version is 3.0.3. Feel free to change this to a later version if you’d like.

      Your screen should look similar to this:

      Screenshot of Insomnia showing one line added to the center pane, which is the code editor.

      Now that you’re familiar with the Insomnia interface, you can begin writing your documentation.

      Step 3 — Getting Started With the OpenAPI Specification

      In this step, you’ll learn more about the OpenAPI Specification. An API Specification can be a JSON or YAML file, but Insomnia only supports YAML. It should have a key called openapi that specifies the version of the OpenAPI Specficiation you’re using.

      According to the specification, here are the fields that can be present at the root of the document:

      Name Type Description
      openapi string REQUIRED. Version of the OpenAPI schema.
      info Info Object REQUIRED. An object containing information about the API.
      servers Array of Server Objects An array containing objects that provide connectivity options to an API server.
      paths Paths objects REQUIRED. An object containing the routes provided by the API, methods, request-bodies, parameters and responses. This is the most important part of the document.
      components Components Object Contains reusable components, meant to reduce file size and keep the docs clean.
      security Array of Security Objects Contains a list of authentication mechanisms for the API. Outside the scope of this tutorial.
      externalDocs External Documentation Object Contains any external documentation for the API

      Don’t worry if this is too much to take in. You’ll be diving deeper into each property, except for security, since that’s outside the scope of this tutorial. The security field defines authentication methods (e.g., username/password, JSON Web Token, or oauth) for a route, but JSONPlaceholder doesn’t have any authentication features.

      Step 4 — Adding the info Object

      In this step, you’ll use the table from Step 1 to begin write your API’s documentation using Insomnia. You’ll start with the info object.

      The info object contains information about the API you’re documenting. This includes things like the title, version of the API, the API’s description, links to its knowledge base (documentation), and its terms-of-service (tos).

      According to the specification, this is what an info object should look like:

      Name Type Description
      title string REQUIRED. The title of the API.
      description string A short description of the API. Markdown can be used here.
      termsOfService string A URL to the Terms of Service for the API.
      contact Contact Object The contact information for the exposed API.
      license License Object The license information for the exposed API.
      version string REQUIRED. The version of the documentation, not the OpenAPI spec.

      The info field has two required properties: the title of the document and the version of the documentation, which should be equal to the version of your API application. The other fields are present for informing the user about your API.

      Now you will add an info object to your documentation using the three most-used fields: title, description, and version. In the Insomnia app, add the following YAML code to Design tab editor:

      info:
        title: JSONPlaceholder
        description: Free fake API for testing and prototyping.
        version: 0.1.0
      

      This is a random version number since JSONPlaceholder doesn’t expose a version number. Feel free to add any other fields to the info object, following the specification from the previous step.

      Warning: YAML is very picky about its indentation. It has to be indented with spaces, and the indent size must be consistent throughout the document.

      Now that you’ve added the info object with basic information about your API, you’ll add the next object: externalDocs.

      Step 5 — Adding the externalDocs Object

      In this step, you will add the externalDocs object. This object contains the link to any other documentation the API might have. An OpenAPI document just defines any routes your API has along with its parameters and responses, so it is usually used as a reference. It is recommended to include separate, human-generated docs that explain each action and guides the user. In JSONPlaceholder’s case, there is a guide.

      According to the specification, here’s what the externalDocs object should look like:

      Field Name Type Description
      description string A short description of the target documentation. Markdown can be used.
      url string REQUIRED. The URL for the target documentation.

      In your YAML document, add an externalDocs object that points to JSONPlaceholder’s guide:

      externalDocs:
        description: "JSONPlaceholder's guide"
        url: https://jsonplaceholder.typicode.com/guide
      

      You should see the changes reflected in the preview pane on the right side of Insomnia.

      Screenshot of Insomnia's preview, showing **JSONPlaceholder**.

      You have now linked to external documentation for your API. Next, you’ll add the servers array.

      Step 6 — Adding the servers Array

      In this step, you’ll add the servers array, which contains any URLs that the API will be hosted at. The documentation you’re creating will be hosted on a different domain from the placeholder API (that is, your documentation will not be hosted on jsonplaceholder.typicode.com). Because of this, you can’t implicitly get the URL for the Try It Out buttons next to the API routes shown in the Insomnia preview.

      To fix this, OpenAPI provides a servers field. Add the following lines to your YAML document:

      servers:
      - url: https://jsonplaceholder.typicode.com
        description: JSONPlaceholder
      

      With that, you now have a way to call the API.

      Step 7 — Adding the paths Object

      In this step, you will add the paths object, which is the heart of your documentation. This object contains all of the routes that are provided by the API. It also contains any parameters, the method, the request body, and all responses of the route.

      Each key of the paths object will be a route (/posts) and the value will be the Path Item object.

      According to the OpenAPI specification, this is what the Path Item object will look like:

      Name Type Description
      summary string An optional summary of this route.
      description string An optional description of what the route can do.
      get/post/put/patch/delete/etc Operation Object A definition of an operation (method) on this route.
      servers Array of Server Objects An alternative server array to service all operations in this path.
      parameters An array of Parameter Object Parameters that are applicable for all operations on this path. These parameters can be on the querystring, header, cookie, or the path itself.

      The Path Item object has a number of fields. The summary and description fields, as their names suggest, provide a short summary and longer description of the path. The servers object is the same as the one in the main OpenAPI document. It defines alternative servers. The parameters object defines any path or query parameters for that path. Each Path Item object can have an operation object. The operation object documents an HTTP method that can be used on this API route.

      The operation object has many items, but for this tutorial, you’ll focus on a smaller set:

      Name Type Description
      tags Array of strings A list of tags for API documentation control. Tags can be used for grouping similar routes.
      summary string A short summary of what the operation does.
      description string A description of the operation. Markdown can be used here.
      externalDocs External Documentation Object Additional external documentation for this operation. Same as externalDocs on the main object.
      parameters Array of Parameter Objects Same as parameters in the Path Item object.
      requestBody Request Body Object The body of the request. This can NOT be used when the method GET or DELETE.
      responses Responses Object REQUIRED. The list of possible responses returned by the API for this operation.

      The tags property groups similar paths. Paths with the same tag will end up in one group. The summary and description fields are the same as the ones in the path object. They allow you to add a short summary and a longer description, respectively. The externalDocs property is the same as that in the main document: it allows you to define any external documentation for that operation.

      The parameters object is the same as the one in the path object. It allows you to define path, query, header, or cookie parameters that have to be sent with the request. The requestBody also allows you to define parameters, but in the body of the request. This requestBody field is only available in POST, PUT and PATCH requests, as defined in the HTTP/1.1 protocol, RFC7231.

      The /posts Route

      Now you will document an API route by creating an object in the paths object. First, you’ll document the /posts route. Begin by adding these lines to your YAML document:

      paths:
        "/posts":
      

      Note: /posts is in quotes because it contains special symbols (/). This is required by YAML so it doesn’t misinterpret the line.

      Next, you need to add a field whose key will be the HTTP method, and whose value will be the Path Item object. Document the GET /posts route, which returns an array of all posts, by adding the highlighted lines:

      paths:
        "/posts":
          get:
            tags: ["posts"]
            summary: Returns all posts.
      

      The tags field groups similar operations together. (Notice how the accordion in the preview is called posts.)

      Next, document the responses one can get back. The only response you’ll get from this API is a 200 response containing an array of all posts.

      An example post that can be returned by JSONPlaceholder will look like this:

      {
        "userId": 1,
        "id": 1,
        "title": "A post's title",
        "body": "The post's content"
      }
      

      Since you’ll be reusing this pretty frequently, you can create a reusable component for this post. This can be done using the components object. You can define the post as a schema in the schemas object, which will be inside the components object. This schema is similar to the schema in a JSON Schema file.

      Add the post schema to your YAML file. Please note that the components object must be placed in the root of the document (without any indentation), not in the paths object.

      components:
        schemas:
          post:
            type: object
            properties:
              id:
                type: number
                description: ID of the post
              title:
                type: string
                description: Title of the post
              body:
                type: string
                description: Body of the post
              userId:
                type: number
                description: ID of the user who created the post
      

      The above schema is an object, denoted by type: object. It has four properties: id, title, body, and userId. That is how a schema is defined. Now you can use $ref in any object to reference this schema. This is defined as per the specification for URI syntax, RFC3986.

      Now that you have the schema, you can add the responses object. This object has items whose value is the status code returned, or default, to catch all other statuses, and the value is a response object. This object contains the description of the response, any headers that are returned, and the response body, along with the Content-Type in the content object.

      Add the responses object to the get operation of the /posts path by copying the highlighted lines:

      paths:
        "/posts":
          get:
            tags: ["posts"]
            summary: Returns all posts.
            responses:
              "200": # 200 Status Code
                description: All went well
                content:
                  application/json: # Reponse is returned in JSON
                    schema:
      

      Be sure to enclose 200 in quotes to make it a string and not a number.

      Here, you’re defining a response that gets returned with the 200 status code, and has a Content-Type header of application/json. In this schema object, you need to pass a reference to the post schema you just created. That can be done with $ref.

      Aside from schema, the application/json object can also contain any examples you wish to give.

      For now, add a reference to the post schema in schema.

      $ref: "#/components/schemas/post"
      

      # refers to the root of the document. Since the post schema is located in components/schemas/post, that’s how you should write it. And since # is a reserved symbol in YAML, you need to enclose the ref in quotes.

      Your Insomnia Design tab should look similar to this:

      Screenshot of Insomnia showing YAML in the center pane and a preview in the right pane.

      You can see that insomnia has rendered a preview of your document. The /posts route has been grouped into a posts section, because of the tag, and the correct response is also showing, as you defined in the schema. The same content-type you defined and the same schema you defined are previewed on the right.

      You can try changing something, like the tag or the responses of the path, and see it update in real time. Be sure to change it back after you’re done.

      Note: Press the Try It Out button in the Path operation in the preview, and then the Execute button to call the JSONPlaceholder API and receive a response.

      With the GET route documented, it’s time to document the POST /posts route. This will be quite similar to the previous operation, but this time, it will be a POST request, hence the object’s key is post (highlighted below). Add the following lines to your YAML file:

      paths:
        "/posts":
          # ...
          post:
            tags: ["posts"]
            summary: Create a new post
            responses:
              "200":
                description: A post was created
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
      

      You’ve just defined another operation. This time, it is a POST request, with the same tag, so it gets grouped along with the GET request you defined earlier. The response also has the same schema since that is what will be returned by JSONPlaceHolder.

      There’s still one thing missing: the post method also accepts a request body. It hasn’t been documented yet, so add the requestBody object to the post operation. The request body is similar to the response object. There’s a description and content field, which are the same as the response object, and there’s also a required field, which is a boolean. This field governs whether a body is required for this request or not. In this case, it is, so add the requestBody object to your operation.

      paths:
        "/posts":
          # ...
          post:
            tags: ["posts"]
            summary: Create a new post
            requestBody:
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/post"
          required: true
            responses:
              "200":
                description: A post was created
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
      

      At this point, your paths object should look like this:

      paths:
        "/posts":
          get:
            tags: ["posts"]
            summary: Returns all posts
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
          post:
            tags: ["posts"]
            summary: Create a new post
            requestBody:
              content:
                application/json:
                  schema:
                    $ref: "#/components/schemas/post"
              required: true
            responses:
              "200":
                description: A post was created
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
      

      In this section, you documented the GET and POST operations available in the /posts route. Next, you’ll document the /posts/:id route, which is used to read, modify, or delete a single post.

      The /posts/:id Route

      Next, you’ll document the /posts/:id route. This route has three operations: GET, PUT, and DELETE. They get a single post, update a post, and delete a post. :id is a dynamic parameter that can be a number (for example: /posts/1, /posts/2, etc.). In OpenAPI, this is denoted as {id}, as shown in the following example:

      paths:
        "/posts":
        # ...
        "/posts/{id}":
        # TODO
      

      The in property defines where the parameter will be placed. This can be in the query string, in the cookie, in the header, or as a part of the path itself. The description is a description of the parameter. The required field is a boolean that indicates if the parameter is required. In the case of path parameters, required has to be true, since the parameter is a part of the path itself.

      Path parameters are special, so they’re defined in the path using braces ({}). The name of the parameter is enclosed in the braces and must match the name in the name field. First, you need to define id as a parameter object in the parameters array. Here’s how you’ll do it:

      paths:
        "/posts/{id}":
          parameters:
          - name: id # Must be same as the value in the {}.
            in: path
            description: ID of the post
            # Since this is in the path, the parameter HAS to be required
            required: true
            # Defining the type of the parameter
            schema:
              # In this case, it is just a string
              type: string
      

      Be sure to include the hyphen (-), otherwise the parameters array would become an object

      The last thing to document are the operations and their responses and request bodies, if they have any. This is similar to what you did in the previous section.

      First, add the GET /posts/:id operation, which gets a single post.

      get:
        tags: ["post"]
        summary: Get a single post
        responses:
          "200":
            description: All went well
            content:
              application/json:
                schema:
                  $ref: "#/components/schemas/post"
          "404":
            description: Post not found
            content:
              application/json:
                schema:
                  type: object
                  properties: {}
      

      Notice that this time, there is a 404 response. This is because the GET request can return a 404 error if the post is not found. The properties: {} in the above code is how you’d define an empty object in YAML.

      Next, add the PUT /posts/:id operation, which updates a post. This method combines the GET and POST methods above, since it has both a requestBody and a 404 response.

      put:
        tags: ["post"]
        summary: Update a post
        requestBody:
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/post"
          required: true
        responses:
          "200":
            description: All went well
            content:
              application/json:
                schema:
                  $ref: "#/components/schemas/post"
          "404":
            description: Post not found
            content:
              application/json:
                schema:
                  type: object
                  properties: {}
      

      JSONPlaceholder doesn’t really validate the data you send it, so there is no 400 or 422 response, but if the API you’re documenting does something like that (which it should), be sure to document those responses as well. To avoid repeating yourself, you can create response components, as you did in the previous section.

      And finally, add the DELETE /posts/:id operation, which deletes a post. This is the same as the GET method, since it returns a 404, but this time, the operation is delete.

      delete:
        tags: ["post"]
        summary: Delete a post
        responses:
          "200":
            description: All went well
            content:
              application/json:
                schema:
                  type: object
                  properties: {}
          "404":
            description: Post not found
            content:
              application/json:
                schema:
                  type: object
                  properties: {}
      

      Note that the DELETE method only returns an empty object ({}), even on a 200 response.

      And with that, you’ve successfully documented the /posts route of JSONPlaceholder. Here’s the full YAML document.

      openapi: 3.0.3
      
      info:
        title: JSONPlaceholder
        description: Free fake API for testing and prototyping.
        version: 0.1.0
      
      externalDocs:
        description: "JSONPlaceholder's guide"
        url: https://jsonplaceholder.typicode.com/guide
      
      servers:
      - url: https://jsonplaceholder.typicode.com
        description: JSONPlaceholder
      
      paths:
        "/posts":
          get:
            tags: ["posts"]
            summary: Returns all posts
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
          post:
            tags: ["posts"]
            summary: Create a new post
            requestBody:
              content:
                application/json:
                  schema:
                    $ref: "#/components/schemas/post"
              required: true
            responses:
              "200":
                description: A post was created
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
        "/posts/{id}":
          parameters:
          - name: id # Must be same as the value in the {}.
            # Location of the parameter.
            # Can be `path`, `cookie`, `query` or `header`
            in: path
            description: ID of the post
            # Since this is in the path, the parameter HAS to be required
            required: true
            # Defining the type of the parameter
            schema:
              # In this case, it is just a string
              type: string
          get:
            tags: ["post"]
            summary: Get a single post
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
              # But this time, you can also get a 404 response,
              # which is an empty JSON object.
              "404":
                description: Post not found
                content:
                  application/json:
                    schema:
                      type: object
                      properties: {}
          put:
            tags: ["post"]
            summary: Update a post
            requestBody:
              content:
                application/json:
                  schema:
                    $ref: "#/components/schemas/post"
              required: true
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
              "404":
                description: Post not found
                content:
                  application/json:
                    schema:
                      type: object
                      properties: {}
          delete:
            tags: ["post"]
            summary: Delete a post
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      type: object
                      properties: {}
              # But this time, you can also get a 404 response,
              # which is an empty JSON object.
              "404":
                description: Post not found
                content:
                  application/json:
                    schema:
                      type: object
                      properties: {}
      
      components:
        schemas:
          post:
            type: object
            properties:
              id:
                type: number
                description: ID of the post
              title:
                type: string
                description: Title of the post
              body:
                type: string
                description: Body of the post
              userId:
                type: number
                description: ID of the user who created the post
      

      In the above document, you’ve documented all /posts routes provided by JSONPlaceholder, and you’ve also covered all HTTP methods that are supported. you’ve also learned about parameters, request bodies, and different responses.

      With the OpenAPI document complete, the next step is to make it available to users.

      Step 8 — Using Redoc to Display API Documentation

      While Insomnia does have a nice-looking Preview pane, you can’t expect all of your users to have Insomnia installed, so you’ll use Redoc to display the OpenAPI YAML file in a nice readable way.

      To build Redoc, you need to have NodeJS installed. (Please note that you don’t need to know any NodeJS or JavaScript to build Redoc.)

      Create a new folder anywhere on your computer. You’ll be building Redoc in this folder and deploy it to GitHub.

      First, you’ll need to save your current OpenAPI document to this folder. Create a new file called openapi.yaml in the current folder and copy-paste the contents in Insomnia’s Design tab to this file. Redoc can now use this file to generate your API documentation

      Next, open a terminal in that folder and run the below command to build Redoc.

      • npx redoc-cli --output index.html bundle openapi.yaml

      npx is the NPM (Node Package Manager)’s CLI tool to fetch a CLI-installable package and run it. This allows you to run redoc-cli without actually installing it to your global $PATH. Instead, it will be available via npx. Be sure to type y if asked to install redoc-cli or not. Next, you’re telling Redoc to bundle the openapi.yaml file you just created into a zero-dependency HTML file, which in this case, will be index.html, since you passed the --output flag.

      This should create a new file called index.html in that directory. This file is the documentation, powered by Redoc. Open the file in your browser and inspect it to make sure that every route you’ve defined is covered.

      Screenshot of the Redoc documentation displayed in a browser.

      Now that you have your documentation site generated, it’s time to make it available to others using GitHub Pages.

      Step 9 — Deploying to GitHub Pages

      Now that you have a documentation website, you can use GitHub Pages to deploy it for the world to see. As part of the prerequisites, you created a new GitHub Repository. Copy the clone URL shown in Quick Setup. You’ll use the git command to push your openapi.yaml and index.html files to GitHub. Be sure to run the below commands in the folder that contains these two files.

      First, you’ll initialize the git repository and commit all of your files:

      • git init
      • git add .
      • git commit -m "First commit" # Feel free to change this message

      Next, you’ll deploy your changes to GitHub. First, you need to tell git that your GitHub repository should be the remote repository. The remote repository is usually stored under the name origin.

      • git remote add origin YOUR_GITHUB_REPO_URL

      And finally, push your changes to GitHub with this command:

      Note: Git has changed the name of the default branch from master to main, so main is used in the command above. Feel free to replace main with master if you like.

      Refresh GitHub, and you should see your two files there.

      Screenshot of two files on GitHub

      Now you need to enable GitHub Pages. Go to your repository’s settings and click the Pages button on the Sidebar. Change the source branch to your default branch (usually main or master) and the folder to / (root) and click Save.

      Finally, visit https://your_username.github.io/your_repo_name, you should see the Redoc page you’ve just created. You can view my version published to GitHub Pages here.

      With that, your API is now available for anyone to see with the URL above.

      Conclusion

      In this tutorial, you documented the /posts route of the JSONPlaceholder API. You documented path parameters as well as request bodies and possible responses. You have also learned to reduce boilerplate using reusable components. Feel free to check out the source code and the live version.

      As a next step, try to document the other routes that JSONPlaceholder offers (e.g., /users), or try this out with your own API. You can go forward from here by using tools like Docasaurus to add documentation that explains and guides the user. Remember to keep your API Spec DRY and easy to read so you can make changes to it in the future. Insomnia also has other features, like the ability to test and debug your API, so be sure to check those out by reading the documentation.



      Source link

      How To Style Common Form Elements with CSS


      The author selected the Diversity in Tech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      Web forms are a common element of website design, ranging in complexity from a search form field to contact forms and complex data filtering. Knowing how to use CSS to style and work with these elements helps provide better solutions to these everyday problems, and can improve your user’s experience of your website.

      This tutorial covers the creation and styling of a web form that requests various data from the user. The form will use text fields, radio buttons, checkboxes, drop-down selections, a text area, and submit and reset buttons. You will create and style this form and its elements
      by resetting the styles with the appearance property, setting up your own consistent style for the form, adding placeholder answers for the text fields, and customizing the radio buttons and checkboxes with various pseudo-classes and pseudo-elements.

      Prerequisites

      Setting Up the Base HTML and CSS

      In this first section, you will set up the HTML and initial styles that you will work with throughout the rest of the tutorial. This HTML will set up the scaffolding of the page and create the form fields you will style later.

      Begin by opening the index.html file in your editor. Then, add the following HTML to provide a base structure for the file:

      index.html

      <!doctype html>
      <html>
        <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <title>CSS Form</title>
          <link rel="stylesheet" href="https://www.digitalocean.com/community/tutorials/styles.css" />
        </head>
        <body>
          <main>
          </main>
        </body>
      </html>
      

      The elements contained in the <head> element define the page’s name with the <title> element and where to load the stylesheet via the <link> element. The <meta> tags define the character encoding and instruct the browser on how to display the site on small-screen device. The main form contents will reside inside the <body> and <main> tags.

      Next, inside the <main> element, create a <form> element. Inside <form> you will add various form elements and <div> elements to help with layout. In this tutorial, additions to code from prior steps are highlighted. Add the highlighted HTML from the following code block to your index.html file:

      index.html

      ...
      <main>
        <form>
          <div>
            <label for="name">Name</label>
            <input id="name" type="text" />
          </div>
          <div>
            <label for="email">Email</label>
            <input id="email" type="email" />
          </div>
          <div>
            <label for="comp">Favorite CSS Compiler</label>
            <select id="comp">
              <option value="sass">Sass</option>
              <option value="less">Less</option>
              <option value="stylus">Stylus</option>
              <option value="postcss">PostCSS</option>
              <option value="other">Other</option>
            </select>
          </div>
          <div>
            <fieldset>
              <legend>Are you familiar with CSS Grid?</legend>
              <input type="radio" name="grid" id="yes" value="yes" />
              <label for="yes">Yes</label>
              <input type="radio" name="grid" id="no" value="no" />
              <label for="no">No</label>
            </fieldset>
          </div>
          <div class="full-width">
            <label for="message">Message</label>
            <textarea id="message"></textarea>
          </div>
          <div class="full-width">
            <input type="checkbox" id="newsletter" />
            <label for="newsletter">Receive our newsletter?</label>
          </div>
          <div class="full-width">
            <button type="submit">Send Response</button>
            <button type="reset">Clear Form</button>
          </div>
        </form>
      </main>
      ...
      

      HTML form structure consists of inter-connected attribute values in order to function correctly. This code creates a form that asks a user for their name and email, asks for their favorite CSS compiler and if they know CSS grid, provides a field for a user-generated message, and has a checkbox to sign the user up for a newsletter. To learn more about how to structure forms in HTML, see the Mozilla Web Docs page on web form structure.

      Be sure to save your changes to index.html, then create a new file in the same directory called styles.css.

      Open styles.css in your text editor. This file provides the styles that the browser will apply to the contents of index.html. Add the following CSS code to your styles.css file:

      styles.css

      body {
        margin: 0;
        background-color: hsl(0, 0%, 98%);
        color: #333;
        font: 100% / normal sans-serif;
      }
      
      main {
        margin: 0 auto;
        padding: 4rem 0;
        width: 90%;
        max-width: 60rem;
      }
      
      form {
        box-sizing: border-box;
        padding: 2rem;
        border-radius: 1rem;
        background-color: hsl(0, 0%, 100%);
        border: 4px solid hsl(0, 0%, 90%);
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 2rem;
      }
      
      .full-width {
        grid-column: span 2;
      }
      

      The body and the main element selectors create some initial text styling and layout for the overall page. The form element selector creates the styles for the overall form container and then defines a CSS Grid consisting of two columns of equal size with grid-template-columns: 1fr 1fr;. Then, the gap: 2rem provides 2rem spacing between each row and column in the grid. Lastly, the .full-width class selector allows containers with this class to extended between the two columns instead of staying in one column.

      Save your changes to styles.css. Next, open a web browser of your choice. Select the File menu item in the browser and then select the Open option. Next, navigate to your project folder and load your index.html file in the browser. The following image demonstrates how the page will render in the browser:

      Rounded white rectangle with a medium gray border on a light gray background. Several form field elements are in the white rectangle with black sans-serif labels.

      The form is displayed in a white box on a light gray background. Each form element is dispersed across the grid with the top four items alternating between the two columns and the last three stacked, spanning the two columns. The default styling of the form elements are as they appear in Firefox; each browser has a different default for styling form elements.

      In this section, you set up the initial HTML and CSS needed to work with form elements. You also learned that each browser handles the styling of these elements in a different way. In the next section, you will use the appearance property to equalize the styling of form elements across all browsers.

      Resetting Form Styling with the appearance Property

      Every browser handles the visual styling of form elements in a different manner. Often the styling of these elements goes beyond the initial capabilities of CSS and follows the aesthetic of the operating system or the browser’s own design language. In order to create consistent styling for all browsers, in this section you will use the appearance property and other properties to remove the default browser styles.

      To begin, open styles.css in your text editor. Create a new group selector consisting of button, fieldset, input, legend, select, and textarea. Then inside the selector block, add the appearance property set to none, as highlighted in the following code block:

      styles.css

      ...
      button,
      fieldset,
      input,
      legend,
      select,
      textarea {
        appearance: none;
      }
      

      The appearance property is the intended way to remove the special styling from form elements. However, due to the age and implementation of this property, most browsers still require a vendor prefix added to the property name. A vendor prefix is a special coded addition prepended to the property name as an identifier for a specific browser. For Chrome, Safari, and recent versions of Edge and Opera, that prefix is -webkit-. Firefox uses the -moz- prefix.

      When working with vendor prefixes, it is important to put the vendor prefix versions of a property first and end with the non-prefixed version. This way, older browsers that only support the prefixed property will use the prefix, but new browsers that support both prefix and the non-prefix versions will use the non-prefixed standard version. Add the highlighted prefix appearance properties as formatted in the following code block:

      styles.css

      legend,
      fieldset,
      select,
      textarea,
      input,
      button {
        -webkit-appearance: none;
        -moz-appearance: none;
        appearance: none;
      }
      

      Save your changes to styles.css and then open index.html in your browser. The appearance properties have removed the embellished styling and have gone to a simpler style, as rendered in the following image:

      Form elements with dark gray borders and middle gray borders creating depth effects.

      The appearance property only allows you to change the browser-specific styling. The biggest change the appearance: none property value made was to remove the radio buttons and checkbox completely. For the rest of the form elements, a few more properties are needed to completely remove the default styles. The highlighted CSS properties in the following code block add the necessary styles to remove the default styles:

      styles.css

      ...
      button,
      fieldset,
      input,
      legend,
      select,
      textarea {
        -webkit-appearance: none;
        -moz-appearance: none;
        appearance: none;
        background-color: transparent;
        border: none;
        padding: 0;
        margin: 0;
        box-sizing: border-box;
      }
      

      This CSS removes the background color and resets the parameters of the box model. Not all of these styles are necessary for all the elements, but it is acceptable to group these reset styles across all the elements.

      Save your changes to styles.css, then refresh index.html in the browser. The form elements have visually disappeared from the page, as shown in the following image:

      Black, sans-serif text in a white rectangle on a gray background.

      In this section, you used appearance and additional properties to completely remove the default styles from the form elements. You also used vendor prefixes to appropriately order apply the property for future browser versions. In the next section, you will begin customizing the visual styling of the form fields.

      Setting Consistent Styles Across Form Fields

      Now that the default browser styles have been completely removed, you will apply a consistent custom aesthetic across all the form elements. This will involve creating various group selectors that target specific form characteristics to receive the appropriate styles.

      To begin, open styles.css in your text editor. Then, create a group selector consisting of input, select, and textarea. Add the highlighted styles from the following code block:

      styles.css

      ...
      legend,
      fieldset,
      select,
      textarea,
      input,
      button {
        ...
      }
      
      input,
      select,
      textarea {
        border: 2px solid #333;
        background-color: white;
        border-radius: 0.25rem;
      }
      

      These styles add a 2px dark gray border around each of the data entry elements, along with a white background and rounded corners.

      Next, you will apply styles for the data entry elements that contain text inside. You will use an attribute selector to specify which input elements to target based on their type attribute value. The highlighted CSS in the following code block provide the styles for the necessary elements:

      styles.css

      ...
      input,
      textarea,
      select {
        ...
      }
      
      input[type="text"],
      input[type="email"],
      select,
      textarea {
        font: 1.25rem / 1.5 sans-serif;
        display: block;
        box-sizing: border-box;
        width: 100%;
        padding: 0.5rem 0.75rem;
      }
      

      These styles apply a consistent font size and family across all elements. The <textarea> element, for example, does not inherit the font settings from the body element. The display, box-sizing, width, and padding properties provide a consistent layout and structure for each of these data entry elements.

      Save your changes to styles.css and then open index.html in your web browser. As rendered in the following image, the fields now have a thicker dark gray border around them and the text of the <select> element is now much larger:

      Black, sans-serif text in a white rectangle on a gray background wth dark gray outline rectangles.

      Next, there are two elements that need some special styling in addition to the broad styling you’ve already written. The first is to give more height to the textarea, and the second is to apply a custom drop-down arrow to the <select> element.

      Return to styles.css and add a textarea element selector. Inside the selector block, create a min-height property set to a value of 10rem. This will create a larger initial area for the form user to fill in text. The highlighted CSS in the following code block illustrates how this is written:

      styles.css

      ...
      input[type="text"],
      input[type="email"],
      select,
      textarea {
        ...
      }
      
      textarea {
        min-height: 10rem;
      }
      

      After adding the styles for the textarea, the next thing to accomplish is to make an image directory. That can be done by running the following in your command prompt from within the same directory as your index.html and styles.css files:

      Next, run the following curl command to download the first image you will work with into the new images directory:

      • curl -sL https://assets.digitalocean.com/articles/68129/down-arrow.svg -o images/down-arrow.svg

      The image you downloaded is an SVG, which is a markup language similar to HTML with the purpose of drawing shapes.

      To add this new image to the <select> element, return to styles.css. Then, create a select element selector and add a background property with the highlighted value from the following code block:

      styles.css

      ...
      textarea {
        min-height: 10rem;
      }
      
      select {
        background: url("images/down-arrow.svg") no-repeat center right 0.75rem;
      }
      

      This background property loads the image into the <select> element’s background and does not repeat the image. Then it centers the image vertically with center and offsets it from the right side with right 0.75rem.

      Save your changes to styles.css, then return to your browser to refresh the page. The <textarea> is now about twice its original height and the <select> element has a blue downward-facing arrow on the right side. The following image illustrates how this is rendered in the browser:

      Black, sans-serif text in a white rectangle on a gray background with dark gray outline rectangles, one of which has a blue downward arrow.

      Throughout this section you created a custom aesthetic for the form’s data entry elements. You also created a background for the <select> element to replace the default arrow. In the next section, you will create custom radio buttons and checkboxes and apply selected states when they are checked.

      Customizing Radio Buttons and Checkboxes with the :checked Pseudo Class

      Now that you have created the base aesthetic of the form, it’s time to apply that visual style to the interactive input items of a radio button and checkbox.

      Start by opening styles.css in your text editor. You will change the input elements with a type attribute of radio or checkbox to have an equal height and width value. Then you will turn the radio buttons into circles. The highlighted portions of the following code block show how this is formatted:

      styles.css

      ...
      select {
        ...
      }
      
      input[type="radio"],
      input[type="checkbox"] {
        height: 1.5em;
        width: 1.5em;
        vertical-align: middle;
      }
      
      input[type="radio"] {
        border-radius: 50%;
      }
      

      The vertical-align property is meant for aligning inline text items. By setting this to middle, the input fields will sit in the middle of the text align. Then, the radio input with a border-radius property of 50% will create a circle because the height and width are the same.

      Save your changes to styles.css and then open index.html in your browser. The two radio buttons and the checkbox are now larger and more noticeable as rendered in the following image:

      Two circles with black sans-serif text and a rounded square with black sans-serif text.

      If you were to interact with the radio buttons or checkbox, nothing visible would happen. The appearance property also removes the selected indicators for these types of input. Next, you will use the :checked psuedo-class selector to apply styles to the selected input item.

      Return to styles.css and create a new selector for the radio button input with a :checked pseudo class. Then, inside the selector block, add a background-image with a radial-gradient so a filled-in style can be applied to selected radio buttons. The highlighted CSS from the following code block shows how this is formatted:

      styles.css

      ...
      input[type="radio"] {
        border-radius: 50%;
      }
      
      input[type="radio"]:checked {
        background-image: radial-gradient(
          hsl(213, 73%, 50%) 40%,
          transparent calc(40% + 1px)
        );
      }
      

      The calc() function allows the transparent property to be set 1px after the color value, creating a solid blue circle inside the input field.

      Save your changes to styles.css and return to index.html in your browser. The radio buttons now have a solid blue circle surrounded by white inside the input. The following image shows the Yes radio button selected:

      Two circles with black sans-serif text and a rounded square with black sans-serif text. One circle has a blue circle inside.

      Next, the checkbox will use a background image to indicate its :checked state, similar to the <select> dropdown’s arrow.

      Run the following curl command to download the checkmark image to your images directory:

      • curl -sL https://assets.digitalocean.com/articles/68129/check.svg -o images/check.svg

      Now that you have the image downloaded and ready to use, return to styles.css in your text editor.

      Next, add a input[type="checkbox"] selector with a :checked pseudo-class attached. In the selector block, add a background property that loads the check.svg image and scales it down to fit in the box. The highlighted CSS in the following code block indicates how this is written:

      stlyes.css

      ...
      input[type="radio"]:checked {
        ...
      }
      
      input[type="checkbox"]:checked {
        background: url('images/check.svg') no-repeat center / 75% auto;
      }
      

      The background property values ensure that the checkmark image is centered to the container, does not repeat, and is scaled down 75% proportionally.

      Save your changes to styles.css then refresh the page in the browser. When selecting the Receive our newsletter check box, now a checkmark appears inside, as illustrated in the following image:

      Square with rounded edge and a blue checkmark inside with black sans-serif text to the right.

      In this section, you created custom radio buttons and input fields and made them adjust their selected state through the use of the :checked pseudo-class. In the next section, you will style the <label> and <legend> elements on the page.

      Adding Special Styles to Labels and Legends

      The next elements to style are the <label> and <legend> elements in the form. There are two different styles that will be used: a small label style for the radio button and checkboxes and a large label style for the remaining elements.

      Open index.html in your text editor. You will add a class attribute to each <label> and <legend> with a value of either large-label or small-label, as highlighted in the following code block:

      index.html

      ...
      <form>
        <div>
          <label for="name" class="large-label">Name</label>
          <input id="name" type="text" />
        </div>
        <div>
          <label for="email" class="large-label">Email</label>
          <input id="email" type="email" />
        </div>
        <div>
          <label for="comp" class="large-label">Favorite CSS Compiler</label>
          ...
        </div>
        <div>
          <fieldset>
            <legend class="large-label">Are you familiar with CSS Grid?</legend>
            <input type="radio" name="grid" id="yes" value="yes" />
            <label for="yes" class="small-label">Yes</label>
            <input type="radio" name="grid" id="no" value="no" />
            <label for="no" class="small-label">No</label>
          </fieldset>
        </div>
        <div class="full-width">
          <label for="message" class="large-label">Message</label>
          <textarea id="message"></textarea>
        </div>
        <div class="full-width">
          <input type="checkbox" id="newsletter" />
          <label for="newsletter" class="small-label">Receive our newsletter?</label>
        </div>
        ...
      </form>
      ...
      

      Save these additions to index.html, then open styles.css in your text editor.

      In styles.css, add a .large-label class selector and add the following properties as highlighted in the following code block:

      styles.css

      ...
      .full-width {
        grid-column: span 2;
      }
      
      .large-label {
        display: inline-block;
        font: bold 1.5rem sans-serif;
        margin-bottom: 0.5rem;
      }
      ...
      

      These styles set the large-label elements to be large and bold with a font size of 1.5rem, which is equivalent to 24px. Then the margin-bottom property provides some space between the label and its counterparts.

      Save this change to styles.css and open index.html in your web browser. The label text above the data entry field will be large and bold, as rendered in the following image:

      Large, bold sans-serif text above black outline input fields.

      Return to styles.css and create another class selector for .small-label. Since these are the labels that are to the right of a radio button or checkbox, they will need some different spacing and sizing styles compared to the .large-label. Add the highlighted CSS to your styles.css from the following code block:

      styles.css

      ...
      .large-label {
        display: inline-block;
        font: bold 1.5rem sans-serif;
        margin-bottom: 0.5rem;
      }
      
      .small-label {
        vertical-align: middle;
        display: inline-block;
        margin-left: 0.25rem;
        margin-right: 1.5rem;
        font: 1.25rem sans-serif;
      }
      ...
      

      The vertical-align: middle will offset the text slightly. The font is set to 1.25rem, the equivalent of 20px, with sans-serif font. The margin properties on the left and right provide space between the input fields and the label.

      Save your updates to styles.css and refresh index.html in the browser. The labels next to the radio buttons and checkbox are now larger and provide more spacing, as rendered in the following image:

      Large, bold sans-serif text above black outline input fields with medium, thinner text.

      In this section, you created styles for two different types of labels based on where they were in relation to their input value. The labels now stand out, providing easier legibility and navigation throughout the form. In the next section, you will provide example data formatting through using the placeholder attribute.

      Providing Placeholder Content with the ::placeholder Pseudo-Element

      Placeholder content on an input or textarea element provides form users with a visual demonstration of what kind of information is being requested and how to format it. The placeholder attribute is added to the HTML with a value describing it. Then the ::placeholder pseudo-element allows for customizing the look of the text.

      To begin making placeholder content, open index.html in your text editor. Add a placeholder attribute to the name text <input />, the email <input />, and the <textarea> elements. The highlighted HTML in the following code block indicates where to add the placeholder and the value to use:

      index.html

      ...
      <form>
        <div>
          <label for="name" class="large-label">Name</label>
          <input id="name" type="text" placeholder="First or Full Name" />
        </div>
        <div>
          <label for="email" class="large-label">Email</label>
          <input id="email" type="email" placeholder="name@example.com" />
        </div>
        ...
        <div class="full-width">
          <label for="message" class="large-label">Message</label>
          <textarea id="message" placeholder="Leave a message…"></textarea>
        </div>
        ...
      </form>
      ...
      

      Save your changes to index.html, then open the page in a web browser. These three text entry areas now have content inside them. Once these text entry fields are selected and content is added, the placeholder text will be removed by the browser. The following image illustrates how the default placeholder styles appear in the browser:

      Black outline containers with light gray sample text inside.

      In order to style the placeholder, open styles.css in your text editor. Add a group selector for input::placeholder and textarea::placeholder. Be sure to use double colons between the selector and the pseudo-element, as this is how the browser recognizes the difference between a pseudo-class and a pseudo-element. The highlighted CSS in the following code block shows how this is written:

      styles.css

      ...
      textarea {
        min-height: 10rem;
      }
      
      input::placeholder,
      textarea::placeholder {
        opacity: 1; /* Firefox */
        color: hsl(213, 73%, 50%);
      }
      
      select {
        background: url("images/down-arrow.svg") no-repeat center right 0.75rem;
      }
      ...
      

      The one thing to note is that Firefox requires an opacity value set to 1 in order to have the full color value. Otherwise Firefox reduces the opacity, dimming the text color and causing accessible color contrast issues depending on the color value. Because this is a Firefox-only situation, a comment is there to explain the purpose of the opacity property’s presence.

      Save your changes to styles.css and return to the browser to refresh index.html. The placeholder text is now the same blue color used for the drop-down arrow and selected states for the radio button and checkbox. The following image illustrates how the browser renders the placeholder content:

      Black outline containers with blue sample text inside.

      With this section, you created placeholder content on text entry fields and styled them with the use of the ::placeholder pseudo-element. In the next section, you will create custom styles for form <button> elements.

      Creating Interactive Button Styles

      In web forms, <button> elements are often used to submit or reset a form. In index.html, there are two buttons, one with a type of submit and the other of reset. Both are functionally useful, but perform opposing actions. The submit button will send the form along to a processor, while the reset button clears all entered data from the form. Due to these different actions, the <button> elements need to look visibly different as well.

      To begin, open styles.css in your text editor and create a button element selector. In this selector block, you will add the styles that are shared between the submit and reset <button> elements, as highlighted in the following code block:

      styles.css

      ...
      input[type="checkbox"]:checked {
        background: url("images/check.svg") no-repeat center / 75% auto;
      }
      
      button {
        font: 1.25rem sans-serif;
        border-radius: 0.25rem;
        cursor: pointer;
        padding: 0.75rem 1.25rem;
      }
      

      The font sets both buttons to have the same font style and size. Then the border-radius adds a rounded corner to both buttons. The cursor property changes the style of the cursor to be the hand-style pointer. Lastly, the padding property defines the space around the inside of the button.

      Save your changes to styles.css and then open index.html in your web browser. The text in the button will grow and the spacing between the text will increase visually due to the padding. The following image shows how the buttons are rendered in the browser:

      Two phrases in dark gray on a white background.

      Next, return to styles.css to add styles for each button type by using an attribute selector targeting each. For the submit button, add a blue background-color and white text color. The reset button will gain a link-like underline and a margin to add more space between the buttons. Add the highlighted CSS from the following code block to your styles.css file:

      styles.css

      ...
      button {
        ...
      }
      
      button[type="submit"] {
        background-color: hsl(213, 73%, 50%);
        color: white;
      }
      
      button[type="reset"] {
        text-decoration: underline;
        margin-left: 1rem;
      }
      

      Save these additions to styles.css then refresh index.html in your text editor. The submit button is now a prominent blue and white, while the reset is subdued underlined text, as rendered in the following image:

      One phrase in white text in a blue container and another phrase in dark gray underlined text on white.

      The <button> element does not have a :hover state by default, so you will now add this to your style. A :hover state is useful to help cursor users have visual feedback that the cursor is situated on the buttons.

      To create :hover states for these <button> elements, return to styles.css in your text editor. Set the submit button’s backgound-color to darken on hover. Then, make the reset button drop the underline when hovered. The highlighted HTML in the following code block indicates how to write these styles:

      styles.css

      ...
      button[type="submit"] {
        background-color: hsl(213, 73%, 50%);
        color: white;
      }
      
      button[type="submit"]:hover {
        background-color: hsl(213, 73%, 40%);
      }
      
      button[type="reset"] {
        text-decoration: underline;
        margin-left: 1rem;
      }
      
      button[type="reset"]:hover {
        text-decoration: none;
      }
      

      Save your changes to styles.css and then return to the browser to refresh index.html. As the following animation portrays, the <button> elements shift their styles when the mouse cursor hovers over them:

      Animation of a blue button becoming darker on hover and underlined text losing the underline on hover.

      In this section, you created styles for the <button> elements so to make their differences visually noticeable. You changed how the cursor appears on hover by adjusting the cursor property. You also created custom styles to apply to each button to provide further visual feedback. In the last section, you will provide further visual activity feedback by creating styles when a form element is presently in use with the :focus pseudo-class.

      Clarifying the Active Form Fields With :focus

      When filling out a form, it is important for the user to know what field they are currently working on. You can accomplish this with the use of the :focus pseudo-class. By default, browsers employ an outline property to indicate when an element has :focus, but at times it may not be a noticeable indicator, or it may clash with other visual aspects of the design. In this section, you will create a custom :focus state that matches your form’s aesthetic.

      To begin working with form field :focus states, open styles.css in your text editor. Create a group selector for input, select, and textarea, all with a :focus pseudo-class, as highlighted in the following code block:

      styles.css

      ...
      input,
      select,
      textarea {
        ...
      }
      
      input:focus,
      select:focus,
      textarea:focus {
        outline: none;
        box-shadow: 0 0 0 4px hsl(213, 90%, 70%);
      }
      
      input[type="text"],
      input[type="email"],
      select,
      textarea {
        ...
      }
      ...
      

      These styles remove the browser default outline value and replace the style with a thick blue stroke created with the box-shadow property. The first three values of the box-shadow are for the shadow’s placement and blur amount. The fourth is called the spread, which in this case creates a 4px stroke around the focused element.

      Next, the buttons will receive a slightly different focus state, since the submit button is the same blue color. The purpose and intent of a focus state is to bring noticeable attention to the focused element, so you will distinguish these outlines in a different ways.

      Add a button:focus selector to styles.css. In the selector block, disable the outline default and add a box-shadow property. The placement, blur, and spread values will remain the same as the entry fields, but the color will be black instead of blue, as highlighted in the following code block:

      styles.css

      ...
      button {
        ...
      }
      
      button:focus {
        outline: none;
        box-shadow: 0 0 0 4px black;
      }
      
      button[type="submit"] {
        ...
      }
      ...
      

      Save your changes to styles.css and return to your browser. Refresh index.html and begin hitting the Tab key for the focus to shift between each element in the form. The following animation shows how the focus style is applied as the focus changes with the Tab key:

      Animation of form fields gaining a light blue outline sequentially.

      In this section, you created focus state styles that provide clear visual feedback when a form element has focus. This visual styling is helpful for mouse, touch, and keyboard input users.

      Conclusion

      Forms are a common element of web design. They allow users to interact with apps, search content, and provide feedback. In this tutorial, you created and styled a full-fledged form. You removed the browser default styles with the appearance property, and created a new custom aesthetic across various elements. You used the :checked pseudo-class to make selected states for radio buttons and checkboxes. Then you added placeholder content and matched the styling with the ::placeholder pseudo-element. After you created custom button styles, you applied :focus styles giving valuable visual interaction feedback to the form users.

      If you would like to read more CSS tutorials, try out the other tutorials in the How To Style HTML with CSS series.



      Source link

      How To Set Up a Video Streaming Server using Nginx-RTMP on Ubuntu 20.04


      Introduction

      There are many use cases for streaming video. Service providers such as Twitch are very popular for handling the web discovery and community management aspects of streaming, and free software such as OBS Studio is widely used for combining video overlays from multiple different stream sources in real time. While these platforms are very powerful, in some cases you may want to be able to host a stream that does not rely on other service providers.

      In this tutorial, you will learn how to configure the Nginx web server to host an independent RTMP video stream that can be linked and viewed in different applications. RTMP, the Real-Time Messaging Protocol, defines the fundamentals of most internet video streaming. You will also learn how to host HLS and DASH streams that support more modern platforms using the same technology.

      Prerequisites

      To complete this guide, you will need:

      This tutorial will use the placeholder domain name your_domain for URLs and hostnames. Substitute this with your own domain name or IP address as you work through the tutorial.

      Step 1 — Installing and Configuring Nginx-RTMP

      Most modern streaming tools support the RTMP protocol, which defines the basic parameters of an internet video stream. The Nginx web server includes a module that allows you to provide an RTMP stream with minimal configuration from a dedicated URL, just like it provides HTTP access to web pages by default. The Nginx RTMP module isn’t included automatically with Nginx, but on Ubuntu 20.04 and most other Linux distributions you can install it as an additional package.

      Begin by running the following commands as a non-root user to update your package listings and install the Nginx module:

      • sudo apt update
      • sudo apt install libnginx-mod-rtmp

      Installing the module won’t automatically start providing a stream. You’ll need to add a configuration block to your Nginx configuration file that defines where and how the stream will be available.

      Using nano or your favorite text editor, open Nginx’s main configuration file, /etc/nginx/nginx.conf, and add this configuration block to the end of the file:

      • sudo nano /etc/nginx/nginx.conf

      /etc/nginx/nginx.conf

      . . .
      rtmp {
              server {
                      listen 1935;
                      chunk_size 4096;
                      allow publish 127.0.0.1;
                      deny publish all;
      
                      application live {
                              live on;
                              record off;
                      }
              }
      }
      
      • listen 1935 means that RTMP will be listening for connections on port 1935, which is standard.
      • chunk_size 4096 means that RTMP will be sending data in 4KB blocks, which is also standard.
      • allow publish 127.0.0.1 and deny publish all mean that the server will only allow video to be published from the same server, to avoid any other users pushing their own streams.
      • application live defines an application block that will be available at the /live URL path.
      • live on enables live mode so that multiple users can connect to your stream concurrently, a baseline assumption of video streaming.
      • record off disables Nginx-RTMP’s recording functionality, so that all streams are not separately saved to disk by default.

      Save and close the file. If you are using nano, press Ctrl+X, then when prompted, Y and Enter.

      This provides the beginning of your RTMP configuration. By default, it listens on port 1935, which means you’ll need to open that port in your firewall. If you configured ufw as part of your initial server setup run the following command.

      Now you can reload Nginx with your changes:

      • sudo systemctl reload nginx.service

      You should now have a working RTMP server. In the next section, we’ll cover streaming video to your RTMP server from both local and remote sources.

      Step 2 — Sending Video to Your RTMP Server

      There are multiple ways to send video to your RTMP server. One option is to use ffmpeg, a popular command line audio-video utility, to play a video file directly on your server. If you don’t have a video file already on the server, you can download one using youtube-dl, a command line tool for capturing video from streaming platforms like YouTube. In order to use youtube-dl, you’ll need an up to date Python installation on your server as well.

      First, install Python and its package manager, pip:

      • sudo apt install python3-pip

      Next, use pip to install youtube-dl:

      Now you can use youtube-dl to download a video from YouTube. If you don’t have one in mind, try this video, introducing DigitalOcean’s App Platform:

      • youtube-dl https://www.youtube.com/watch?v=iom_nhYQIYk

      You’ll see some output as youtube-dl combines the video and audio streams it’s downloading back into a single file – this is normal.

      Output

      [youtube] iom_nhYQIYk: Downloading webpage WARNING: Requested formats are incompatible for merge and will be merged into mkv. [download] Destination: Introducing App Platform by DigitalOcean-iom_nhYQIYk.f137.mp4 [download] 100% of 32.82MiB in 08:40 [download] Destination: Introducing App Platform by DigitalOcean-iom_nhYQIYk.f251.webm [download] 100% of 1.94MiB in 00:38 [ffmpeg] Merging formats into "Introducing App Platform by DigitalOcean-iom_nhYQIYk.mkv" Deleting original file Introducing App Platform by DigitalOcean-iom_nhYQIYk.f137.mp4 (pass -k to keep) Deleting original file Introducing App Platform by DigitalOcean-iom_nhYQIYk.f251.webm (pass -k to keep)

      You should now have a video file in your current directory with a title like Introducing App Platform by DigitalOcean-iom_nhYQIYk.mkv. In order to stream it, you’ll want to install ffmpeg:

      And use ffmpeg to send it to your RTMP server:

      • ffmpeg -re -i "Introducing App Platform by DigitalOcean-iom_nhYQIYk.mkv" -c:v copy -c:a aac -ar 44100 -ac 1 -f flv rtmp://localhost/live/stream

      This ffmpeg command is doing a few things to prepare the video for a streaming-friendly format. This isn’t an ffmpeg tutorial, so you don’t need to examine it too closely, but you can understand the various options as follows:

      • -re specifies that input will be read at its native framerate.
      • -i "Introducing App Platform by DigitalOcean-iom_nhYQIYk.mkv" specifies the path to our input file.
      • -c:v is set to copy, meaning that you’re copying over the video format you got from YouTube natively.
      • -c:a has other parameters, namely aac -ar 44100 -ac 1, because you need to resample the audio to an RTMP-friendly format. aac is a widely supported audio codec, 44100 hz is a common frequency, and -ac 1 specifies the first version of the AAC spec for compatibility purposes.
      • -f flv wraps the video in an flv format container for maximum compatibility with RTMP.

      The video is sent to rtmp://localhost/live/stream because you defined the live configuration block in Step 1, and stream is an arbitrarily chosen URL for this video.

      Note: You can learn more about ffmpeg options from ffmprovisr, a community-maintained catalog of ffmpeg command examples, or refer to the official documentation.

      While ffmpeg is streaming the video, it will print timecodes:

      Output

      frame= 127 fps= 25 q=-1.0 size= 405kB time=00:00:05.00 bitrate= 662.2kbits/s speed=frame= 140 fps= 25 q=-1.0 size= 628kB time=00:00:05.52 bitrate= 931.0kbits/s speed=frame= 153 fps= 25 q=-1.0 size= 866kB time=00:00:06.04 bitrate=1173.1kbits/s speed=

      This is standard ffmpeg output. If you were converting video to a different format, these might be helpful in order to understand how efficiently the video is being resampled, but in this case, you just want to see that it’s being played back consistently. Using this sample video, you should get exact fps= 25 increments.

      While ffmpeg is running, you can connect to your RTMP stream from a video player. If you have VLC, mpv, or another media player installed locally, you should be able to view your stream by opening the URL rtmp://your_domain/live/stream in your media player. Your stream will terminate after ffmpeg has finished playing the video. If you want it to keep looping indefinitely, you can add -stream_loop -1 to the beginning of your ffmpeg command.

      Note: You can also stream directly to, for example, Facebook Live using ffmpeg without needing to use Nginx-RTMP at all by replacing rtmp://localhost/live/stream in your ffmpeg command with rtmps://live-api-s.facebook.com:443/rtmp/your-facebook-stream-key. YouTube uses URLs like rtmp://a.rtmp.youtube.com/live2. Other streaming providers that can consume RTMP streams should behave similarly.

      Now that you’ve learned to stream static video sources from the command line, you’ll learn how to stream video from dynamic sources using OBS on a desktop.

      Step 3 — Streaming Video to Your Server via OBS (Optional)

      Streaming via ffmpeg is convenient when you have a prepared video that you want to play back, but live streaming can be much more dynamic. The most popular software for live streaming is OBS, or Open Broadcaster Software – it is free, open source, and very powerful.

      OBS is a desktop application, and will connect to your server from your local computer.

      After installing OBS, configuring it means customizing which of your desktop windows and audio sources you want to add to your stream, and then adding credentials for a streaming service. This tutorial will not be covering your streaming configuration, as it is down to preference, and by default, you can have a working demo by just streaming your entire desktop. In order to set your streaming service credentials, open OBS’ settings menu, navigate to the Stream option and input the following options:

      Streaming Service: Custom
      Server: rtmp://your_domain/live
      Play Path/Stream Key: obs_stream
      

      obs_stream is an arbitrarily chosen path – in this case, your video would be available at rtmp://your_domain/live/obs_stream. You do not need to enable authentication, but you do need to add an additional entry to the IP whitelist that you configured in Step 1.

      Back on the server, open Nginx’s main configuration file, /etc/nginx/nginx.conf, and add an additional allow publish entry for your local IP address. If you don’t know your local IP address, it’s best to just go to a site like What’s my IP which can tell you where you accessed it from:

      • sudo nano /etc/nginx/nginx.conf

      /etc/nginx/nginx.conf

      . . .
                      allow publish 127.0.0.1;
                      allow publish your_local_ip_address;
                      deny publish all;
      . . .
      

      Save and close the file, then reload Nginx:

      • sudo systemctl reload nginx.service

      You should now be able to close OBS’ settings menu and click Start Streaming from the main interface! Try viewing your stream in a media player as before. Now that you’ve seen the fundamentals of streaming video in action, you can add a few other features to your server to make it more production-ready.

      Step 4 — Adding Monitoring to Your Configuration (Optional)

      Now that you have Nginx configured to stream video using the Nginx-RTMP module, a common next step is to enable the RTMP statistics page. Rather than adding more and more configuration details to your main nginx.conf file, Nginx allows you to add per-site configurations to individual files in a subdirectory called sites-available/. In this case, you’ll create one called rtmp:

      • sudo nano /etc/nginx/sites-available/rtmp

      Add the following contents:

      /etc/nginx/sites-available/rtmp

      server {
          listen 8080;
          server_name  localhost;
      
          # rtmp stat
          location /stat {
              rtmp_stat all;
              rtmp_stat_stylesheet stat.xsl;
          }
          location /stat.xsl {
              root /var/www/html/rtmp;
          }
      
          # rtmp control
          location /control {
              rtmp_control all;
          }
      }
      

      Save and close the file. The stat.xsl file from this configuration block is used to style and display an RTMP statistics page in your browser. It is provided by the libnginx-mod-rtmp library that you installed earlier, but it comes zipped up by default, so you will need to unzip it and put it in the /var/www/html/rtmp directory to match the above configuration. Note that you can find additional information about any of these options in the Nginx-RTMP documentation.

      Create the /var/www/html/rtmp directory, and then uncompress the stat.xsl.gz file with the following commands:

      • sudo mkdir /var/www/html/rtmp
      • sudo gunzip -c /usr/share/doc/libnginx-mod-rtmp/examples/stat.xsl.gz > /var/www/html/rtmp/stat.xsl`

      Finally, to access the statistics page that you added, you will need to open another port in your firewall. Specifically, the listen directive is configured with port 8080, so you will need to add a rule to access Nginx on that port. However, you probably don’t want others to be able to access your stats page, so it’s best only to allow it for your own IP address. Run the following command:

      • sudo ufw allow from your_ip_address to any port http-alt

      Next, you’ll need to activate this new configuration. Nginx’s convention is to create symbolic links (like shortcuts) from files in sites-available/ to another folder called sites-enabled/ as you decide to enable or disable them. Using full paths for clarity, make that link:

      • sudo ln -s /etc/nginx/sites-available/rtmp /etc/nginx/sites-enabled/rtmp

      Now you can reload Nginx again to process your changes:

      • sudo systemctl reload nginx.service

      You should now be able to go to http://your_domain:8080/stat in a browser to see the RTMP statistics page. Visit and refresh the page while streaming video and watch as the stream statistics change.

      You’ve now seen how to monitor your video stream and push it to third party providers. In the final section, you’ll learn how to provide it directly in a browser without the use of third party streaming platforms or standalone media player apps.

      Step 5 — Creating Modern Streams for Browsers (Optional)

      As a final step, you may want to add support for newer streaming protocols so that users can stream video from your server using a web browser directly. There are two protocols that you can use to create HTTP-based video streams: Apple’s HLS and MPEG DASH. They both have advantages and disadvantages, so you will probably want to support both.

      The Nginx-RTMP module supports both standards. To add HLS and DASH support to your server, you will need to modify the rtmp block in your nginx.conf file. Open /etc/nginx/nginx.conf using nano or your preferred editor, then add the following highlighted directives:

      • sudo nano /etc/nginx/nginx.conf

      /etc/nginx/nginx.conf

      . . .
      rtmp {
              server {
      . . .
                      application live {
                              live on;
                              record off;
                              hls on;
                              hls_path /var/www/html/stream/hls;
                              hls_fragment 3;
                              hls_playlist_length 60;
      
                              dash on;
                              dash_path /var/www/html/stream/dash;
                      }
              }
      }
      . . .
      

      Save and close the file. Next, add this to the bottom of your sites-available/rtmp:

      • sudo nano /etc/nginx/sites-available/rtmp

      /etc/nginx/sites-available/rtmp

      . . .
      server {
          listen 8088;
      
          location / {
              add_header Access-Control-Allow-Origin *;
              root /var/www/html/stream;
          }
      }
      
      types {
          application/dash+xml mpd;
      }
      

      Note: The Access-Control-Allow-Origin * header enables CORS, or Cross-Origin Resource Sharing, which is disabled by default. This communicates to any web browsers accessing data from your server that the server may load resources from other ports or domains. CORS is needed for maximum compatibility with HLS and DASH clients, and a common configuration toggle in many other web deployments.

      Save and close the file. Note that you’re using port 8088 here, which is another arbitrary choice for this tutorial to ensure we aren’t conflicting with any services you may be running on port 80 or 443. You’ll want to open that port in your firewall for now too:

      Finally, create a stream directory in your web root to match the configuration block, so that Nginx can generate the necessary files for HLS and DASH:

      • sudo mkdir /var/www/html/stream

      Reload Nginx again:

      • sudo systemctl reload nginx

      You should now have an HLS stream available at http://your_domain:8088/hls/stream.m3u8 and a DASH stream available at http://your_domain:8088/dash/stream.mpd. These endpoints will generate any necessary metadata on top of your RTMP video feed in order to support modern APIs.

      Conclusion

      The configuration options that you used in this tutorial are all documented in the Nginx RTMP Wiki page. Nginx modules typically share common syntax and expose a very large set of configuration options, and you can review their documentation to change any of your settings from here.

      Nearly all internet video streaming is implemented on top of RTMP, HLS, and DASH, and by using the approach that you have explored in this tutorial, you can provide your stream via other broadcasting services, or expose it any other way you choose. Next, you could look into configuring Nginx as a reverse proxy in order to make some of these different video endpoints available as subdomains.



      Source link