No Time Dad

A blog about web development written by a busy dad

Responsive Blog Starter Template with Tailwindcss Part 1

I’ve become a little obsessed with building templates lately. I think it’s a great way to sharpen my skills while creating something of value that I can use later. I’ve done a lot of Tailwindcss dashboard templates recently, so I thought it’d be fun to make a Tailwindcss blog starter template.

I definitely want this blog starter template to be responsive. The template shouldn’t be overly complicated. It should be easy to extend and modify, but still have some basic features.

This is the first part of a two part series for this blog template. In this first part I’m going to focus on the smaller screen size view, up to Tailwind’s md (768px) breakpoint. The second part of the series will handle the larger screen size view, from Tailwind’s md breakpoint on up.

You can checkout the demo and final code for this first part here. The full responsive demo can be viewed here.

Thinking ahead

When starting a new project I like to take a step back and think about the features and requirements. This is basically “napkin math” that helps me plan out what I’m going to build. This initial plan should be extremely flexible. Requirements always change.

I think that this blog starter template will consist of the following sections:

  1. 1) Header section containing blog title and navigation.
  2. 2) Hero section with large text or an image to draw the user in.
  3. 3) Blog posts section that contains recent or popular posts.
  4. 4) Footer section that has the blog title again and maybe social media links.

The structural markup (using React) will look something like this:

const BlogStarterTemplate = () => (
  <>
    <HeaderSection />
    <HeroSection />
    <BlogPostsSection />
    <FooterSection />
  </>
);

Now I have a basic outline to work from. I can add and remove sections as needed, but I think this is likely going to be the final structure for this blog template.

Building

I like to work from the top down. Maybe this is obvious to most front-end developers, but I’ve definitely made the mistake of implementing a content section or a footer section before a header section and it caused problems for me later.

The other important thing to keep in mind here is that I’m designing for the small screen view. If I start to work in larger screen styles at the same time, things could get confusing. I’ve made this mistake a bunch of times too. I’ve found that it’s better to focus on one screen size at a time.

I’m going to be using React components here, but it’s simply to make it easier to break up the large page and explain each section. I won’t be using any state, so the React components could easily be translated to vanilla html or any other framework.

Header section

When the header is viewed on smaller screens, all of the navigation links aren’t going to fit on a single row. There a few different ways to deal with this. One of the more common ways is to implement a drop-down menu. But, this comes a few issues. The main one being accessibility. Most developers, including myself aren’t very good at creating accessible elements, and that’s especially true for drop-down menus.

Another issue with drop-down menus is that I’m going to need to track the state of the mobile menu. Which isn’t the end of the world since I’m already using React here, but I really just want to focus on the elements and not worry about tracking and updating state values.

For this basic starter template I’ve chosen to simply move the menu under the main title. This is a really simple solution that I actually don’t mind at all. The navigation remains accessible and I didn’t have to track the state of a menu.

I’ll use a flexbox container to help with alignment via Tailwind’s flex and flex-col utility classes. I’ll try to have the same text color and hover state for links throughout the page.

const BlogHeaderSection = () => (
  <div className="flex flex-col p-4">
    <a href="#home" className="text-2xl font-bold no-underline text-gray-800 hover:text-gray-600">Cycle Blog</a>
    <nav className="flex flex-col pt-4 space-y-2">
      <a className="no-underline text-gray-800 hover:text-gray-600 font-semibold" href="#home">Home</a>
      <a className="no-underline text-gray-800 hover:text-gray-600 font-semibold" href="#about">About</a>
      <a className="no-underline text-gray-800 hover:text-gray-600 font-semibold" href="#contact">Contact</a>
      <a className="no-underline text-gray-800 hover:text-gray-600 font-semibold" href="#projects">Projects</a>
    </nav>
  </div>
);
Hero section

The hero section is likely what visitors to the site will first see when they load the page. I think it’s important to have large text here to give a brief summary on what the blog is about. An elevator pitch, essentially.

The header section for the blog starter template has three parts. The main header, the subtext, and an image. On small screens (as shown below) these are all stacked on top of each other, and the spacing between them is handled by Tailwind’s space-y-3 utility class.

The hero section has the same p-4 padding applied to match the header section. Spacing will get more complicated the page evolves, and I think it’s a good idea to set the padding in a top-level parent div whenever possible. Unfortunately, it wasn’t really possible for me here. Or at least, I couldn’t find a clever way to make it work without having to use negative margins.

const BlogHeroSection = () => (
  <div className="bg-gray-100 p-4 space-y-3">
    <div className="text-4xl font-extrabold">A blog about cycling</div>
    <div className="opacity-70">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div>
    <img className="max-w-full h-auto rounded-md" src={cyclist} alt="cyclist" />
  </div> 
);
A blog about cycling
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
cyclist
Blog posts section

The next section is where the blog posts themselves will go. On small screens this will just be a single column. On larger screens, I’ll likely initialize a grid with 2 or 3 columns. More on that in the second part of this series.

Before I can build this section I need to figure out what the blog post element will look like. This is the element for each blog post featured in the blog post section. It’s pretty common to use cards for this, so that’s what I’ll do here too.

The Post component is a card that uses Tailwind’s shadow-md along with rounded borders to define it’s shape. It’s using a gray background in the header, but ideally that would be replaced by an image. The Post component also has an effect that increases the shadow-md to shadow-lg on hover.

const Post = () => (
  <div className="shadow-md rounded-br-md rounded-bl-md hover:shadow-lg">
    <a href="#blog-post" className="no-underline">
      <div className="max-w-full h-36 bg-gray-200 rounded-tr-md rounded-tl-md"></div>
    </a>
    <div className="p-2 space-y-1">
      <a href="#blog-post" className="text-xl font-bold no-underline text-gray-800 hover:text-gray-600">Lorem ipsum</a>
      <div className="opacity-70">Jan 21, 2021</div>
      <div className="opacity-85">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...</div>
    </div>
  </div>
);
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...

Now that I have a basic blog post card, I can populate the blog posts section. For this starter page, I do this using a generic map call. But, on an actual site it would be populated by the contents of a directory. Gatsby uses graphql.

const BlogPostsSection = () => (
  <div className="p-4">
    <div className="text-2xl font-bold py-4">Posts</div>
    {/* Create 3 blog posts */}
    <div className="space-y-6">{[...Array(3)].map((_, i) => <Post key={i} />)}</div>
    <div className="flex justify-between pt-6">
      <PreviousButton />
      <NextButton />
    </div>
  </div>
);

const Post = () => (
  <div className="shadow-md rounded-br-md rounded-bl-md hover:shadow-lg">
    <a href="#blog-post" className="no-underline">
      <div className="max-w-full h-36 bg-gray-200 rounded-tr-md rounded-tl-md"></div>
    </a>
    <div className="p-2 space-y-1">
      <a href="#blog-post" className="text-xl font-bold no-underline text-gray-800 hover:text-gray-600">Lorem ipsum</a>
      <div className="opacity-70">Jan 21, 2021</div>
      <div className="opacity-85">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...</div>
    </div>
  </div>
);

const NextButton = () => (
  <button type="button" className="flex-shrink-0 border border-gray-500 text-base font-semibold py-2 px-4 rounded-lg shadow-md hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-200">
    <div className="flex items-center">
      More
      <ArrowSmRightSvg />
    </div>
  </button>
);

const PreviousButton = () => (
  <button type="button" className="flex-shrink-0 border border-gray-500 text-base font-semibold py-2 px-4 rounded-lg shadow-md hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-200">
    <div className="flex items-center">
      <ArrowSmLeftSvg />
      Back
    </div>
  </button>
);

const ArrowSmRightSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 ml-2" viewBox="0 0 20 20" fill="currentColor">
  <path fillRule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>

const ArrowSmLeftSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
  <path fillRule="evenodd" d="M9.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L7.414 9H15a1 1 0 110 2H7.414l2.293 2.293a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
Posts
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Footer section

Last but not least, I have the footer section. Again, nothing too fancy is happening here. The title of the blog is displayed in large letters, as well as the brief description of the site. This section also uses Tailwind’s space-y-3 utility class to help with spacing between the elements.

After the title and summary I added in some “social” svg icons. Originally, I used actuall Twitter, YouTube, and Instagram icons but trying to change their color was getting annoying so I went with a few Heroicons svg icons instead. As shown in the code below, I like to create components for svg icons. I find it much easier to work with them this way.

const FooterSection = () => (
  <div className="bg-gray-100 p-4 space-y-3">
    <a href="#home" className="text-4xl font-extrabold no-underline text-gray-800 hover:text-gray-600">Cycling Blog</a>
    <div className="opacity-70">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div>
    <div className="flex items-center space-x-2">
      <a href="#email" aria-label="email"><EmailSvg /></a>
      <a href="#chat" aria-label="chat"><ChatSvg /></a>
      <a href="#phone" aria-label="call"><PhoneSvg /></a>
    </div>
  </div>
);

const EmailSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 hover:text-gray-800" viewBox="0 0 20 20" fill="currentColor">
  <path fillRule="evenodd" d="M14.243 5.757a6 6 0 10-.986 9.284 1 1 0 111.087 1.678A8 8 0 1118 10a3 3 0 01-4.8 2.401A4 4 0 1114 10a1 1 0 102 0c0-1.537-.586-3.07-1.757-4.243zM12 10a2 2 0 10-4 0 2 2 0 004 0z" clipRule="evenodd" />
</svg>

const ChatSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 hover:text-gray-800" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z" />
</svg>

const PhoneSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 hover:text-gray-800" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
Cycling Blog
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Conclusion

So, that’s all four sections completed for the small screen view. In the next post in this series I’ll be modifying the code above to handle larger screens. The structure and elements will mostly remain the same. That is sort of the beauty of mobile first design, especially using Tailwindcss.

Checkout part 2 of the series here.

Demo & final code
A blog about cycling
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
cyclist
Posts
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Lorem ipsum
Jan 21, 2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...
Cycling Blog
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
// package.json
...
"react": "^17.0.2",
"tailwindcss": "^2.1.2"
...
// blog-starter-template.jsx
import React from 'react';
import cycling from './cycling.jpg';

const BlogStarterTemplate = () => (
  <>
    <HeaderSection />
    <HeroSection />
    <BlogPostsSection />
    <FooterSection />
  </>
);

const HeaderSection = () => (
  <div className="flex flex-col p-4 bg-white">
    <a href="#home" className="text-2xl font-bold no-underline text-gray-800 hover:text-gray-600">Cycle Blog</a>
    <nav className="flex flex-col pt-4 space-y-2">
      <a className="no-underline text-gray-800 hover:text-gray-600 font-semibold" href="#home">Home</a>
      <a className="no-underline text-gray-800 hover:text-gray-600 font-semibold" href="#about">About</a>
      <a className="no-underline text-gray-800 hover:text-gray-600 font-semibold" href="#contact">Contact</a>
      <a className="no-underline text-gray-800 hover:text-gray-600 font-semibold" href="#projects">Projects</a>
    </nav>
  </div>
);

const HeroSection = () => (
  <div className="bg-gray-100 p-4 space-y-3">
    <div className="text-4xl font-extrabold">A blog about cycling</div>
    <div className="opacity-70">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div>
    <img className="max-w-full h-auto rounded-md" src={cyclist} alt="cyclist" />
  </div>
);

const BlogPostsSection = () => (
  <div className="p-4">
    <div className="text-2xl font-bold py-4">Posts</div>
    <div className="space-y-6">{[...Array(10)].map((_, i) => <Post key={i} />)}</div>
    <div className="flex justify-between pt-6">
      <PreviousButton />
      <NextButton />
    </div>
  </div>
);

const FooterSection = () => (
  <div className="bg-gray-100 p-4 space-y-3 w-96">
    <a href="#home" className="text-4xl font-extrabold no-underline text-gray-800 hover:text-gray-600">Cycling Blog</a>
    <div className="opacity-70">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div>
    <div className="flex items-center space-x-2">
      <a href="#email" aria-label="email"><EmailSvg /></a>
      <a href="#chat" aria-label="chat"><ChatSvg /></a>
      <a href="#phone" aria-label="call"><PhoneSvg /></a>
    </div>
  </div>
);

const NextButton = () => (
  <button type="button" className="flex-shrink-0 border border-gray-500 text-base font-semibold py-2 px-4 rounded-lg shadow-md hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-200">
    <div className="flex items-center">
      More
      <ArrowSmRightSvg />
    </div>
  </button>
);

const PreviousButton = () => (
  <button type="button" className="flex-shrink-0 border border-gray-500 text-base font-semibold py-2 px-4 rounded-lg shadow-md hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-200">
    <div className="flex items-center">
      <ArrowSmLeftSvg />
      Back
    </div>
  </button>
);

const ArrowSmRightSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 ml-2" viewBox="0 0 20 20" fill="currentColor">
  <path fillRule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>

const ArrowSmLeftSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
  <path fillRule="evenodd" d="M9.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L7.414 9H15a1 1 0 110 2H7.414l2.293 2.293a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>

const EmailSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 hover:text-gray-800" viewBox="0 0 20 20" fill="currentColor">
  <path fillRule="evenodd" d="M14.243 5.757a6 6 0 10-.986 9.284 1 1 0 111.087 1.678A8 8 0 1118 10a3 3 0 01-4.8 2.401A4 4 0 1114 10a1 1 0 102 0c0-1.537-.586-3.07-1.757-4.243zM12 10a2 2 0 10-4 0 2 2 0 004 0z" clipRule="evenodd" />
</svg>

const ChatSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 hover:text-gray-800" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z" />
</svg>

const PhoneSvg = () => <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 hover:text-gray-800" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>

export default BlogStarterTemplate;