DEV Community

Cover image for Adding efficient Blog Search Functionality with Tailwind CSS and React Nextjs
Mohammad Ezzeddin Pratama
Mohammad Ezzeddin Pratama

Posted on

Adding efficient Blog Search Functionality with Tailwind CSS and React Nextjs

In my recent project portfolio, I focused on improving the user experience by implementing a real-time search filter for blog content. This feature allows users to quickly find relevant posts by filtering through blog titles and descriptions as they type, resulting in a more efficient and user-friendly interface.

State Management with useState

The first step in building the search functionality was to manage the user’s input dynamically. For this, I used React’s useState hook to store the search term:

const [searchTerm, setSearchTerm] = useState<string>('');
Enter fullscreen mode Exit fullscreen mode

This state variable, searchTerm, holds the current value of the text entered by the user in the search input field. As the user types, this state is updated in real-time, providing the foundation for the filtering logic.

Filtering Logic with useEffect

To filter the list of blogs based on the search term, I used the useEffect hook. This ensures that every time the searchTerm or the list of blogs (blogs) changes, the filtering logic is executed:

useEffect(() => {
  const searchTermLower = searchTerm.toLowerCase();
  const filtered = blogs.filter((blog: BlogItem) => 
    blog.title.toLowerCase().includes(searchTermLower) ||
    blog.description.toLowerCase().includes(searchTermLower)
  );
  setFilteredBlogs(filtered);
}, [searchTerm, blogs]);
Enter fullscreen mode Exit fullscreen mode

Here's how it works:

  • Normalization: The search term and the blog data are converted to lowercase to make the search case-insensitive.
  • Filtering: The filter method is applied to the blogs array, returning only those blog items where the title or description contains the search term.
  • Updating State: The filtered list is stored in a new state variable, filteredBlogs, ensuring the UI only displays relevant results.

Designing the Input Component with Tailwind CSS

The next step was to design an intuitive and responsive input field using Tailwind CSS. One challenge was ensuring the input field had a distinct border color that changes when focused, especially in different themes like dark mode.

Here’s how I implemented it:

<div className="relative w-40">
  <div className="absolute inset-0 border-4 border-blue-500 rounded-lg pointer-events-none transition-all duration-200 focus-within:border-blue-700"></div>
  <input
    type="text"
    placeholder="Search blogs..."
    value={searchTerm}
    onChange={(e) => setSearchTerm(e.target.value)}
    className="w-full h-full p-2 rounded-lg bg-black/[.05] dark:bg-white/[.05] text-black/80 dark:text-white/80 focus:outline-none"
  />
</div>
Enter fullscreen mode Exit fullscreen mode
  • Wrapper div: This div wraps the input field and contains the customizable border.
    • border-4 border-blue-500: Adds a thick blue border by default.
    • focus-within:border-blue-700: Changes the border color to a darker blue when the input field is focused, providing a clear visual indication.
  • Input Field:
    • w-full h-full: Ensures the input field occupies the full width and height of its container, creating a seamless design.
    • focus:outline-none: Removes the default outline to rely solely on the custom border effect.

Bringing It All Together

The combination of useState for managing input, useEffect for filtering logic, and Tailwind CSS for styling, results in a robust and visually appealing search functionality. Here’s a brief walkthrough of how it all connects:

  1. User Input: As the user types in the search box, searchTerm is updated in real-time.
  2. Filtering: The useEffect hook triggers, filtering the blog list based on the updated searchTerm.
  3. Rendering: The filtered list (filteredBlogs) is rendered, showing only the relevant blog posts.
  4. Responsive Design: The input field and search results are styled using Tailwind CSS, ensuring the interface remains clean and user-friendly across all devices.

Conclusion

Full of my code in Blog.tsx

'use client'

import { fetcher } from '../../../services/fetcher';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import useSWR from 'swr';
import { useEffect, useState } from 'react';

import EmptyState from '@/common/components/elements/EmptyState';
import LoadingCard from '@/common/components/elements/LoadingCard';
import { DEVTO_BLOG_API } from '@/common/constants';
import { BlogItem } from '@/common/types/blog';

import { useBlogView } from '../../../stores/blog-view';
import useIsMobile from '../../../hooks/useIsMobile';

import BlogCard from './BlogCard';
import BlogListHeader from './BlogListHeader';

export default function Blog() {
  const isMobile = useIsMobile();
  const [searchTerm, setSearchTerm] = useState<string>('');
  const [filteredBlogs, setFilteredBlogs] = useState<BlogItem[]>([]);
  const { viewOption, setViewOption } = useBlogView();

  const { data, isLoading } = useSWR(DEVTO_BLOG_API, fetcher, {
    revalidateOnMount: true,
  });

  const blogs: BlogItem[] = data?.filter((blog: BlogItem) => blog.collection_id === null) || [];

  // UseEffect untuk memperbarui filteredBlogs setiap kali searchTerm atau blogs berubah
  useEffect(() => {
    const searchTermLower = searchTerm.toLowerCase();
    const filtered = blogs.filter((blog: BlogItem) => 
      blog.title.toLowerCase().includes(searchTermLower) ||
      blog.description.toLowerCase().includes(searchTermLower)
    );
    setFilteredBlogs(filtered);
  }, [searchTerm, blogs]);

  if (isLoading) {
    return (
      <div
        className={clsx(
          'gap-5 sm:gap-4',
          viewOption === 'list' || isMobile ? 'flex flex-col' : 'grid grid-cols-2 sm:!gap-5'
        )}
      >
        {[1, 2].map((item) => (
          <LoadingCard key={item} view={viewOption} />
        ))}
      </div>
    );
  }

  if (blogs.length === 0 && !isLoading) {
    return <EmptyState message="No Data" />;
  }

  return (
    <>
      {!isMobile && <BlogListHeader viewOption={viewOption} setViewOption={setViewOption} />}
      <div
        className={clsx(
          'mt-6 gap-5 sm:gap-4',
          viewOption === 'list' || isMobile ? 'flex flex-col' : 'grid grid-cols-2'
        )}
      >
        <input
          type="text"
          placeholder="Search blogs..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          className="w-40 rounded-lg border border-neutral-300 dark:border-neutral-600 text-black/80 dark:text-white/80 bg-black/[.05] dark:bg-white/[.05] col-span-full hover:bg-black/10 dark:hover:bg-white/10 focus:border-blue-500 dark:focus:border-blue-500 focus:outline-none p-2"
        />
        {filteredBlogs.map((item: BlogItem, index: number) => (
          <motion.div
            key={item.id}
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            transition={{ duration: 0.3, delay: index * 0.1 }}
          >
            <BlogCard view={viewOption} {...item} />
          </motion.div>
        ))}
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

This enhanced search functionality not only improves the user experience by making content more accessible but also showcases the flexibility of combining React and Tailwind CSS. Whether dealing with large datasets or simply aiming to provide a more intuitive user interface, this approach ensures high performance and a seamless user experience.

Top comments (0)