blocks UI Components

repository·main·Indexed 23 days ago

https://github.com/ephraimduncan/blocks

A collection of accessible and customizable UI components that can be integrated into applications via the shadcn CLI using a remote registry. The library provides a variety of pre-built blocks including AI chat interfaces, command menus, dialogs, file uploads, form layouts, grid lists, login screens, onboarding flows, sidebars, stats displays, and tables.

Tokens
109.8K
Snippets
72
Records
102
Agent score
83%

What's inside blocks

  1. Implement a split progress bar for Commands

    main

    The MetricCard component has a specialized rendering mode for the 'Commands' metric. When title === 'Commands' and a details array is provided, the component calculates a split progress bar based on the 'Writes' and 'Reads' values.

    To use this, ensure your details array contains two objects where the first represents 'Writes' and the second represents 'Reads'. The component will parse these strings (removing commas) to calculate the relative percentages for an emerald (Writes) and blue (Reads) segmented bar.

    <MetricCard
      title="Commands"
      value="13.8M"
      limit="Unlimited"
      percentage={67}
      progressColor="bg-blue-500"
      actionLabel="Upgrade"
      actionIcon={<Box className="h-4 w-4" />}
      details=[
        { label: 'Writes', value: '11,276,493', color: 'bg-emerald-500' },
        { label: 'Reads', value: '2,548,921', color: 'bg-blue-500' },
      ]
    />
  2. Implement a Command Menu with keyboard shortcuts

    main

    You can build a searchable command menu (often called a 'Command Palette') by combining a Dialog component with a Command component. This pattern allows users to quickly navigate a site or execute actions via a centralized interface.

    Key Features to Implement:

    • Keyboard Shortcuts: Use a useEffect hook to listen for global keydown events (e.g., Cmd+K or /) to toggle the menu visibility.
    • Searchable Items: Use CommandGroup to categorize items and CommandItem for individual actions. Use the keywords prop on CommandItem to improve search relevance.
    • Action Execution: Use the onSelect callback on CommandItem to trigger navigation (e.g., via router.push) or other side effects like copying text to the clipboard.
    • Accessibility: Wrap the command interface in a Dialog with a DialogHeader containing a DialogTitle and DialogDescription (even if visually hidden via sr-only) to ensure screen readers understand the context.
    // Example of implementing keyboard shortcuts for the menu
    useEffect(() => {
      const down = (e: KeyboardEvent) => {
        if ((e.key === 'k' && (e.metaKey || e.ctrlKey)) || e.key === '/') {
          // Prevent opening if user is already typing in an input
          if (
            (e.target instanceof HTMLElement && e.target.isContentEditable) ||
            e.target instanceof HTMLInputElement ||
            e.target instanceof HTMLTextAreaElement ||
            e.target instanceof HTMLSelectElement
          ) {
            return;
          }
          e.preventDefault();
          setOpen((prev) => !prev);
        }
      };
    
      document.addEventListener('keydown', down);
      return () => document.removeEventListener('keydown', down);
    }, []);
    
    // Example of a searchable CommandItem
    <CommandItem
      key={item.href}
      keywords={item.keywords}
      onSelect={() => {
        runCommand(() => router.push(item.href));
      }}
      value={`Navigation ${item.label}`}
    >
      <IconArrowRight aria-hidden="true" className="size-4" />
      {item.label}
    </CommandItem>
  3. Import Sidebar blocks from the library

    main

    You can import various sidebar component variations (Sidebar01 through Sidebar06) from the sidebar module. Note that some components are exported directly from their directory, while others are exported from an app/page path within their directory structure.

    export { default as Sidebar01 } from './sidebar-01';
    export { default as Sidebar02 } from './sidebar-02';
    export { default as Sidebar03 } from './sidebar-03';
    export { default as Sidebar04 } from './sidebar-04/app/page';
    export { default as Sidebar05 } from './sidebar-05/app/page';
    export { default as Sidebar06 } from './sidebar-06/app/page';
  4. Add blocks using the shadcn CLI

    main

    Once the registry is configured in components.json, you can add specific blocks to your project using the npx shadcn@latest add command followed by the block identifier.

    Examples:

    • To add a login block: @blocks-so/login-01
    • To add a dialog block: @blocks-so/dialog-01
    • To add a sidebar block: @blocks-so/sidebar-01
    # Add a specific block
    npx shadcn@latest add @blocks-so/login-01
    
    # Add a dialog block
    npx shadcn@latest add @blocks-so/dialog-01
    
    # Add a sidebar block
    npx shadcn@latest add @blocks-so/sidebar-01
  5. Add blocks via direct registry URL

    main

    If you do not want to configure the registry in components.json, you can add a block directly by providing its full registry URL to the shadcn CLI.

    # Using the direct registry URL
    npx shadcn@latest add https://blocks.so/r/login-01.json
  6. Configure the blocks registry in components.json

    main

    To use blocks from the @blocks-so registry via the shadcn CLI, you must first add the remote registry configuration to your components.json file. This allows you to reference blocks using the @blocks-so/ prefix.

    {
      "registries": {
        "@blocks-so": "https://blocks.so/r/{name}.json"
      }
    }
  7. Understand the FileTreeItem structure

    main

    The fileTree property uses a discriminated union of FileItem and FolderItem to represent a directory structure.

    FileItem

    Represents a single file.

    • type: Must be 'file'.
    • name: The filename.
    • path: The file path.
    • content: The string content of the file.

    FolderItem

    Represents a directory.

    • type: Must be 'folder'.
    • name: The folder name.
    • path: The folder path.
    • children: An array of FileTreeItem[] representing the contents of the folder.
  8. Implement the GridList03 component pattern

    main

    The GridList03 pattern is a responsive grid of interactive cards used for displaying action items, settings, or navigation links. It utilizes a data array to map through items, rendering each within a Card component.

    Data Structure

    To implement this pattern, define an array of objects (e.g., actions) where each object contains:

    • title: The heading for the card.
    • description: A brief summary of the action.
    • href: The destination URL.
    • icon: A component (typically from lucide-react) to be rendered.
    • iconForeground: Tailwind CSS class for the icon color.
    • iconBackground: Tailwind CSS class for the icon container background.
    • ringColorClass: Tailwind CSS class for the icon's ring color.

    Layout Behavior

    • Mobile: A single-column stack with minimal spacing.
    • Small Screens (sm): A 2-column grid.
    • Large Screens (lg): A 3-column grid.
    • Interactivity: The cards use focus-within:ring-2 for accessibility and include a decorative ArrowUpRight icon that reacts to the group-hover state.
    import {
      ArrowRight,
      ArrowUpRight,
      CheckCircle,
      ContactRound,
      Hand,
      Server,
      UserCircle,
    } from 'lucide-react';
    import { Card, CardContent } from '@/components/ui/card';
    import { cn } from '@/lib/utils';
    
    const actions = [
      {
        title: 'Getting Started',
        description: 'Everything you need to know to get started and get to work in ChatCloud.',
        href: '#',
        icon: ArrowRight,
        iconForeground: 'text-green-700',
        iconBackground: 'bg-green-50 dark:bg-green-950/30',
        ringColorClass: 'ring-green-700/30',
      },
      // ... other action objects
    ];
    
    export default function GridList03() {
      return (
        <div className="flex items-center justify-center p-8">
          <div className="grid grid-cols-1 space-y-0.5 overflow-hidden rounded-2xl bg-muted p-0.5 shadow-sm sm:grid-cols-2 sm:gap-0.5 sm:space-y-0 lg:grid-cols-3">
            {actions.map((action) => (
              <Card
                className={cn(
                  'group relative rounded-xl border-0 bg-card p-0 shadow-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-inset'
                )}
                key={action.title}
              >
                <CardContent className="p-6">
                  <div>
                    <span
                      className={cn(
                        action.iconBackground,
                        action.iconForeground,
                        'inline-flex rounded-lg p-3 ring-2 ring-inset',
                        action.ringColorClass
                      )}
                    >
                      <action.icon aria-hidden="true" className="h-6 w-6" />
                    </span>
                  </div>
                  <div className="mt-4">
                    <h3 className="text-balance font-semibold text-base text-foreground">
                      <a className="focus:outline-none" href={action.href}>
                        <span aria-hidden="true" className="absolute inset-0" />
                        {action.title}
                      </a>
                    </h3>
                    <p className="mt-2 text-pretty text-muted-foreground text-sm">
                      {action.description}
                    </p>
                  </div>
                  <span
                    aria-hidden="true"
                    className="pointer-events-none absolute top-6 right-6 text-muted-foreground/50 group-hover:text-muted-foreground/60"
                  >
                    <ArrowUpRight className="h-6 w-6" />
                  </span>
                </CardContent>
              </Card>
            ))}
          </div>
        </div>
      );
    }
  9. Implement the Login03 block

    main

    The Login03 block is a client-side React component designed for a centered login form. It utilizes standard UI components for Button, Input, and Label. The form includes fields for email and password, a submit button, and a link for password resets.

    Note: This component uses 'use client' and assumes the existence of a @/components/ui/ directory containing the necessary primitive components.

    'use client';
    
    import { Button } from '@/components/ui/button';
    import { Input } from '@/components/ui/input';
    import { Label } from '@/components/ui/label';
    
    export default function Login03() {
      return (
        <div className="flex min-h-dvh items-center justify-center">
          <div className="flex flex-1 flex-col justify-center px-4 py-10 lg:px-6">
            <div className="sm:mx-auto sm:w-full sm:max-w-sm">
              <h3 className="text-balance text-center font-semibold text-foreground text-lg dark:text-foreground">
                Welcome Back
              </h3>
              <p className="text-pretty text-center text-muted-foreground text-sm dark:text-muted-foreground">
                Enter your credentials to access your account.
              </p>
              <form action="#" className="mt-6 space-y-4" method="post">
                <div>
                  <Label
                    className="font-medium text-foreground text-sm dark:text-foreground"
                    htmlFor="email-login-03"
                  >
                    Email
                  </Label>
                  <Input
                    autoComplete="email"
                    className="mt-2"
                    id="email-login-03"
                    name="email-login-03"
                    placeholder="ephraim@blocks.so"
                    type="email"
                  />
                </div>
                <div>
                  <Label
                    className="font-medium text-foreground text-sm dark:text-foreground"
                    htmlFor="password-login-03"
                  >
                    Password
                  </Label>
                  <Input
                    autoComplete="password"
                    className="mt-2"
                    id="password-login-03"
                    name="password-login-03"
                    placeholder="**************"
                    type="password"
                  />
                </div>
                <Button className="mt-4 w-full py-2 font-medium" type="submit">
                  Sign in
                </Button>
              </form>
              <p className="mt-6 text-pretty text-muted-foreground text-sm dark:text-muted-foreground">
                Forgot your password?{' '} 
                <a
                  className="font-medium text-primary hover:text-primary/90 dark:text-primary dark:hover:text-primary/90"
                  href="#"
                >
                  Reset password
                </a>
              </p>
            </div>
          </div>
        </div>
      );
    }
  10. Implement a File Upload component with drag-and-drop

    main

    You can build a file upload interface using React that supports both clicking to browse and drag-and-drop functionality. The implementation uses a hidden <input type="file" /> triggered by a container's onClick event and handles file drops via onDrop and onDragOver handlers.

    Key features to implement:

    • Drag and Drop: Use e.preventDefault() on onDragOver to allow dropping, and access files via e.dataTransfer.files in onDrop.
    • File Selection: Use a useRef<HTMLInputElement> to programmatically trigger the file browser when the upload area is clicked.
    • Progress Simulation: Track upload progress per file using a state object (e.g., Record<string, number>) where the key is the filename.
    • File Previews: Generate temporary URLs for uploaded images using URL.createObjectURL(file) and ensure you clean them up using URL.revokeObjectURL(imageUrl) to prevent memory leaks.
    'use client';
    
    import { HelpCircle, Trash2, Upload } from 'lucide-react';
    import { useRef, useState } from 'react';
    import { Button } from '@/components/ui/button';
    import { Card, CardContent } from '@/components/ui/card';
    import { Input } from '@/components/ui/input';
    import { Label } from '@/components/ui/label';
    import {
      Select,
      SelectContent,
      SelectGroup,
      SelectItem,
      SelectTrigger,
      SelectValue,
    } from '@/components/ui/select';
    import {
      Tooltip,
      TooltipContent,
      TooltipProvider,
      TooltipTrigger,
    } from '@/components/ui/tooltip';
    import { cn } from '@/lib/utils';
    
    export default function FileUpload01() {
      const fileInputRef = useRef<HTMLInputElement>(null);
      const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
      const [fileProgresses, setFileProgresses] = useState<Record<string, number>>(
        {}
      );
    
      const handleFileSelect = (files: FileList | null) => {
        if (!files) return;
    
        const newFiles = Array.from(files);
        setUploadedFiles((prev) => [...prev, ...newFiles]);
    
        // Simulate upload progress for each file
        newFiles.forEach((file) => {
          let progress = 0;
          const interval = setInterval(() => {
            progress += Math.random() * 10;
            if (progress >= 100) {
              progress = 100;
              clearInterval(interval);
            }
            setFileProgresses((prev) => ({
              ...prev,
              [file.name]: Math.min(progress, 100),
            }));
          }, 300);
        });
      };
    
      const handleBoxClick = () => {
        fileInputRef.current?.click();
      };
    
      const handleDragOver = (e: React.DragEvent) => {
        e.preventDefault();
      };
    
      const handleDrop = (e: React.DragEvent) => {
        e.preventDefault();
        handleFileSelect(e.dataTransfer.files);
      };
    
      const removeFile = (filename: string) => {
        setUploadedFiles((prev) => prev.filter((file) => file.name !== filename));
        setFileProgresses((prev) => {
          const newProgresses = { ...prev };
          delete newProgresses[filename];
          return newProgresses;
        });
      };
    
      return (
        <div className="flex items-center justify-center p-10">
          <Card className="mx-auto w-full max-w-lg rounded-lg bg-background p-0 shadow-md">
            <CardContent className="p-0">
              <div className="p-6 pb-4">
                <div className="flex items-start justify-between">
                  <div>
                    <h2 className="text-balance font-medium text-foreground text-lg">
                      Create a new project
                    </h2>
                    <p className="mt-1 text-pretty text-muted-foreground text-sm">
                      Drag and drop files to create a new project.
                    </p>
                  </div>
                </div>
              </div>
    
              <div className="mt-2 px-6 pb-4">
                <div className="grid grid-cols-2 gap-4">
                  <div>
                    <Label className="mb-2" htmlFor="projectName">
                      Project name
                    </Label>
                    <Input
                      defaultValue="Open Source Stripe"
                      id="projectName"
                      type="text"
                    />
                  </div>
    
                  <div>
                    <Label className="mb-2" htmlFor="projectLead">
                      Project lead
                    </Label>
                    <Select
                      defaultValue="1"
                      items={{
                        '1': 'Ephraim Duncan',
                        '2': 'Lucas Smith',
                        '3': 'Timur Ercan',
                      }}
                    >
                      <SelectTrigger className="w-full ps-2" id="projectLead">
                        <SelectValue placeholder="Select project lead" />
                      </SelectTrigger>
                      <SelectContent>
                        <SelectGroup>
                          <SelectItem value="1">
                            <img
                              alt="Ephraim Duncan"
                              className="size-5 rounded"
                              height={20}
                              src="https://blocks.so/avatar-01.png"
                              width={20}
                            />
                            <span className="truncate">Ephraim Duncan</span>
                          </SelectItem>
                          <SelectItem value="2">
                            <img
                              alt="Lucas Smith"
                              className="size-5 rounded"
                              height={20}
                              src="https://blocks.so/avatar-03.png"
                              width={20}
                            />
                            <span className="truncate">Lucas Smith</span>
                          </SelectItem>
                          <SelectItem value="3">
                            <img
                              alt="Timur Ercan"
                              className="size-5 rounded"
                              height={20}
                              src="https://blocks.so/avatar-02.jpg"
                              width={20}
                            />
                            <span className="truncate">Timur Ercan</span>
                          </SelectItem>
                        </SelectGroup>
                      </SelectContent>
                    </Select>
                  </div>
                </div>
              </div>
    
              <div className="px-6">
                <div
                  className="flex cursor-pointer flex-col items-center justify-center rounded-md border-2 border-border border-dashed p-8 text-center"
                  onClick={handleBoxClick}
                  onDragOver={handleDragOver}
                  onDrop={handleDrop}
                >
                  <div className="mb-2 rounded-full bg-muted p-3">
                    <Upload className="h-5 w-5 text-muted-foreground" />
                  </div>
                  <p className="text-pretty font-medium text-foreground text-sm">
                    Upload a project image
                  </p>
                  <p className="mt-1 text-pretty text-muted-foreground text-sm">
                    or,{' '} 
                    <label
                      className="cursor-pointer font-medium text-primary hover:text-primary/90"
                      htmlFor="fileUpload"
                      onClick={(e) => e.stopPropagation()}
                    >
                      click to browse
                    </label>{' '} 
                    (4MB max)
                  </p>
                  <input
                    accept="image/*"
                    className="hidden"
                    id="fileUpload"
                    onChange={(e) => handleFileSelect(e.target.files)}
                    ref={fileInputRef}
                    type="file"
                  />
                </div>
              </div>
    
              <div
                className={cn(
                  'space-y-3 px-6 pb-5',
                  uploadedFiles.length > 0 ? 'mt-4' : ''
                )}
              >
                {uploadedFiles.map((file, index) => {
                  const imageUrl = URL.createObjectURL(file);
    
                  return (
                    <div
                      className="flex flex-col rounded-lg border border-border p-2"
                      key={file.name + index}
                      onLoad={() => {
                        return () => URL.revokeObjectURL(imageUrl);
                      }}
                    >
                      <div className="flex items-center gap-2">
                        <div className="row-span-2 flex h-14 w-18 items-center justify-center self-start overflow-hidden rounded-sm bg-muted">
                          <img
                            alt={file.name}
                            className="h-full w-full object-cover"
                            src={imageUrl}
                          />
                        </div>
    
                        <div className="flex-1 pr-1">
                          <div className="flex items-center justify-between">
                            <div className="flex items-center gap-2">
                              <span className="max-w-[250px] truncate text-foreground text-sm">
                                {file.name}
                              </span>
                              <span className="whitespace-nowrap text-muted-foreground text-sm">
                                {Math.round(file.size / 1024)} KB
                              </span>
                            </div>
                            <Button
                              className="bg-transparent! hover:text-red-500"
                              onClick={() => removeFile(file.name)}
                              size="icon-sm"
                              variant="ghost"
                            >
                              <Trash2 className="h-4 w-4" />
                            </Button>
                          </div>
    
                          <div className="flex items-center gap-2">
                            <div className="h-2 flex-1 overflow-hidden rounded-full bg-muted">
                              <div
                                className="h-full bg-primary"
                                style={{
                                  width: `${fileProgresses[file.name] || 0}%`,
                                }}
                              />
                            </div>
                            <span className="whitespace-nowrap text-muted-foreground text-xs">
                              {Math.round(fileProgresses[file.name] || 0)}%
                            </span>
                          </div>
                        </div>
                      </div>
                    </div>
                  );
                })}
              </div>
    
              <div className="flex items-center justify-between rounded-b-lg border-border border-t bg-muted px-6 py-3">
                <TooltipProvider delay={0}>
                  <Tooltip>
                    <TooltipTrigger
                      render={
                        <Button
                          className="flex items-center text-muted-foreground hover:text-foreground"
                          size="sm"
                          variant="ghost"
                        />
                      }
                    >
                      <HelpCircle className="mr-1 h-4 w-4" />
                      Need help?
                    </TooltipTrigger>
                    <TooltipContent className="border bg-background py-3 text-foreground">
                      <div className="space-y-1">
                        <p className="text-pretty font-medium text-[13px]">
                          Need assistance?
                        </p>
                        <p className="max-w-[200px] text-pretty text-muted-foreground text-xs dark:text-muted-background">
                          Upload project images by dragging and dropping files or
                          using the file browser. Supported formats: JPG, PNG, SVG.
                          Maximum file size: 4MB.
                        </p>
                      </div>
                    </TooltipContent>
                  </Tooltip>
                </TooltipProvider>
    
                <div className="flex gap-2">
                  <Button
                    className="h-9 px-4 font-medium text-sm"
                    variant="outline"
                  >
                    Cancel
                  </Button>
                  <Button className="h-9 px-4 font-medium text-sm">Continue</Button>
                </div>
              </div>
            </CardContent>
          </Card>
        </div>
      );
    }