DEV Community

agenthustler
agenthustler

Posted on

Dribbble Scraping: Extract Design Portfolios and Creative Work Data

Web scraping design platforms like Dribbble opens up a treasure trove of creative data — from designer portfolios and shot metadata to color palettes and engagement metrics. Whether you're building a design trends analyzer, a talent discovery tool, or a competitive intelligence dashboard for creative agencies, extracting data from Dribbble programmatically can save you hundreds of hours of manual research.

In this comprehensive guide, we'll explore how Dribbble is structured, what data points are available, and how to build efficient scrapers using both Python and Node.js. We'll also look at how cloud-based scraping platforms like Apify can handle Dribbble extraction at scale without worrying about rate limits or infrastructure.

Understanding Dribbble's Structure

Before writing any scraping code, it's important to understand how Dribbble organizes its content. Dribbble is structured around several key entities:

Shots

Shots are the core content unit on Dribbble. Each shot is a design piece (image, animation, or video) uploaded by a designer. A shot contains:

  • Title and description: The name and context of the design work
  • Images: The actual design files in multiple resolutions (teaser, 1x, 2x, and sometimes animated GIFs)
  • Tags: Categorization labels applied by the designer
  • Color palette: Extracted dominant colors from the design (typically 5-7 hex values)
  • Engagement metrics: Likes (appreciations), views, saves (buckets), and comments
  • Published date: When the shot was uploaded
  • Attachments: Additional files the designer may have shared

Designer Profiles

Each designer has a profile page containing:

  • Bio and location: Professional description and geographic information
  • Skills and specializations: Tags indicating areas of expertise
  • Social links: Connected accounts (Twitter, GitHub, personal website)
  • Team membership: Whether they belong to a design team
  • Follower/following counts: Social graph metrics
  • Shot portfolio: All their published work

Collections and Teams

Designers can organize shots into collections (buckets), and multiple designers can form teams with shared portfolios.

Data Points Worth Extracting

When scraping Dribbble, here are the most valuable data points to target:

Shot Metadata

shot_data = {
    "id": 12345678,
    "title": "Mobile Banking App Dashboard",
    "description": "A clean, modern dashboard design for...",
    "html_url": "https://dribbble.com/shots/12345678",
    "width": 1600,
    "height": 1200,
    "images": {
        "hidpi": "https://cdn.dribbble.com/..._2x.png",
        "normal": "https://cdn.dribbble.com/..._1x.png",
        "teaser": "https://cdn.dribbble.com/..._teaser.png"
    },
    "published_at": "2026-03-15T10:30:00Z",
    "tags": ["mobile", "banking", "dashboard", "fintech", "ui"],
    "colors": ["#1a73e8", "#ffffff", "#f5f5f5", "#333333", "#4caf50"],
    "likes_count": 342,
    "views_count": 15420,
    "comments_count": 28,
    "saves_count": 89,
    "animated": False
}
Enter fullscreen mode Exit fullscreen mode

Designer Profile Data

designer_data = {
    "id": 987654,
    "name": "Sarah Chen",
    "username": "sarahdesigns",
    "bio": "Product designer at...",
    "location": "San Francisco, CA",
    "pro": True,
    "followers_count": 12500,
    "following_count": 340,
    "shots_count": 156,
    "skills": ["UI Design", "UX Design", "Mobile Design"],
    "social_links": {
        "twitter": "https://twitter.com/sarahdesigns",
        "website": "https://sarahchen.design"
    },
    "created_at": "2019-06-15T00:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Building a Dribbble Scraper with Python

Let's build a practical scraper using Python with BeautifulSoup and requests. Since Dribbble loads some content dynamically, we'll need to handle both static HTML parsing and API-like endpoints.

Setting Up the Environment

# requirements.txt
requests>=2.31.0
beautifulsoup4>=4.12.0
lxml>=5.1.0
Enter fullscreen mode Exit fullscreen mode

Basic Shot Scraper

import requests
from bs4 import BeautifulSoup
import json
import time
import re

class DribbbleScraper:
    BASE_URL = "https://dribbble.com"

    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                          "AppleWebKit/537.36 (KHTML, like Gecko) "
                          "Chrome/120.0.0.0 Safari/537.36",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.5",
        })

    def scrape_shots_listing(self, category="popular", page=1):
        """Scrape shots from a listing page."""
        url = f"{self.BASE_URL}/shots/{category}?page={page}"
        response = self.session.get(url)
        response.raise_for_status()

        soup = BeautifulSoup(response.text, "lxml")
        shots = []

        for shot_element in soup.select("li.shot-thumbnail"):
            shot = self._parse_shot_thumbnail(shot_element)
            if shot:
                shots.append(shot)

        return shots

    def _parse_shot_thumbnail(self, element):
        """Extract data from a shot thumbnail element."""
        try:
            link = element.select_one("a.shot-thumbnail-link")
            if not link:
                return None

            shot_url = link.get("href", "")
            shot_id = self._extract_shot_id(shot_url)

            # Get the image
            img = element.select_one("img")
            image_url = img.get("src", "") if img else ""

            # Get title
            title_el = element.select_one(".shot-title")
            title = title_el.get_text(strip=True) if title_el else ""

            # Get designer info
            designer_el = element.select_one(".display-name")
            designer_name = designer_el.get_text(strip=True) if designer_el else ""

            # Get engagement metrics
            likes_el = element.select_one(".js-shot-likes-count")
            likes = self._parse_count(likes_el.get_text(strip=True)) if likes_el else 0

            views_el = element.select_one(".js-shot-views-count")
            views = self._parse_count(views_el.get_text(strip=True)) if views_el else 0

            return {
                "id": shot_id,
                "title": title,
                "url": f"{self.BASE_URL}{shot_url}" if not shot_url.startswith("http") else shot_url,
                "image_url": image_url,
                "designer": designer_name,
                "likes": likes,
                "views": views
            }
        except Exception as e:
            print(f"Error parsing shot: {e}")
            return None

    def scrape_shot_detail(self, shot_url):
        """Scrape detailed information from a single shot page."""
        response = self.session.get(shot_url)
        response.raise_for_status()

        soup = BeautifulSoup(response.text, "lxml")

        # Extract structured data from JSON-LD if available
        json_ld = soup.select_one('script[type="application/ld+json"]')
        structured_data = {}
        if json_ld:
            try:
                structured_data = json.loads(json_ld.string)
            except json.JSONDecodeError:
                pass

        # Extract color palette
        colors = []
        color_elements = soup.select(".color-chip")
        for color_el in color_elements:
            hex_color = color_el.get("title") or color_el.get_text(strip=True)
            if hex_color:
                colors.append(hex_color)

        # Extract tags
        tags = []
        tag_elements = soup.select(".shot-tags-container a.tag")
        for tag_el in tag_elements:
            tags.append(tag_el.get_text(strip=True))

        # Extract description
        desc_el = soup.select_one(".shot-description")
        description = desc_el.get_text(strip=True) if desc_el else ""

        return {
            "colors": colors,
            "tags": tags,
            "description": description,
            "structured_data": structured_data
        }

    def scrape_designer_profile(self, username):
        """Scrape a designer's profile page."""
        url = f"{self.BASE_URL}/{username}"
        response = self.session.get(url)
        response.raise_for_status()

        soup = BeautifulSoup(response.text, "lxml")

        name_el = soup.select_one(".profile-name")
        name = name_el.get_text(strip=True) if name_el else username

        bio_el = soup.select_one(".profile-bio")
        bio = bio_el.get_text(strip=True) if bio_el else ""

        location_el = soup.select_one(".profile-location")
        location = location_el.get_text(strip=True) if location_el else ""

        # Extract follower/following counts
        stats = {}
        stat_elements = soup.select(".profile-stats li")
        for stat in stat_elements:
            label = stat.select_one(".stat-label")
            value = stat.select_one(".stat-value")
            if label and value:
                stats[label.get_text(strip=True).lower()] = self._parse_count(
                    value.get_text(strip=True)
                )

        return {
            "username": username,
            "name": name,
            "bio": bio,
            "location": location,
            "followers": stats.get("followers", 0),
            "following": stats.get("following", 0),
            "shots_count": stats.get("shots", 0)
        }

    @staticmethod
    def _extract_shot_id(url):
        match = re.search(r"/shots/(\d+)", url)
        return int(match.group(1)) if match else None

    @staticmethod
    def _parse_count(text):
        text = text.strip().lower().replace(",", "")
        if "k" in text:
            return int(float(text.replace("k", "")) * 1000)
        elif "m" in text:
            return int(float(text.replace("m", "")) * 1000000)
        try:
            return int(text)
        except ValueError:
            return 0


# Usage example
if __name__ == "__main__":
    scraper = DribbbleScraper()

    # Scrape popular shots
    shots = scraper.scrape_shots_listing("popular", page=1)
    print(f"Found {len(shots)} shots")

    for shot in shots[:5]:
        print(f"  {shot['title']} - {shot['likes']} likes, {shot['views']} views")

        # Get detailed info with rate limiting
        if shot['url']:
            details = scraper.scrape_shot_detail(shot['url'])
            print(f"    Colors: {details['colors']}")
            print(f"    Tags: {details['tags']}")
            time.sleep(1)  # Be respectful with rate limiting
Enter fullscreen mode Exit fullscreen mode

Building a Dribbble Scraper with Node.js

For JavaScript developers, here's a Node.js approach using Cheerio and Axios:

const axios = require('axios');
const cheerio = require('cheerio');

class DribbbleScraper {
    constructor() {
        this.baseUrl = 'https://dribbble.com';
        this.client = axios.create({
            headers: {
                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
                'Accept': 'text/html,application/xhtml+xml',
                'Accept-Language': 'en-US,en;q=0.5'
            },
            timeout: 30000
        });
    }

    async scrapeShotsListing(category = 'popular', page = 1) {
        const url = `${this.baseUrl}/shots/${category}?page=${page}`;
        const { data } = await this.client.get(url);
        const $ = cheerio.load(data);

        const shots = [];

        $('li.shot-thumbnail').each((_, element) => {
            const $el = $(element);
            const link = $el.find('a.shot-thumbnail-link');
            const shotUrl = link.attr('href') || '';

            const img = $el.find('img');
            const titleEl = $el.find('.shot-title');
            const designerEl = $el.find('.display-name');
            const likesEl = $el.find('.js-shot-likes-count');
            const viewsEl = $el.find('.js-shot-views-count');

            shots.push({
                id: this.extractShotId(shotUrl),
                title: titleEl.text().trim(),
                url: shotUrl.startsWith('http') ? shotUrl : `${this.baseUrl}${shotUrl}`,
                imageUrl: img.attr('src') || '',
                designer: designerEl.text().trim(),
                likes: this.parseCount(likesEl.text().trim()),
                views: this.parseCount(viewsEl.text().trim())
            });
        });

        return shots;
    }

    async scrapeShotDetail(shotUrl) {
        const { data } = await this.client.get(shotUrl);
        const $ = cheerio.load(data);

        // Extract colors
        const colors = [];
        $('.color-chip').each((_, el) => {
            const color = $(el).attr('title') || $(el).text().trim();
            if (color) colors.push(color);
        });

        // Extract tags
        const tags = [];
        $('.shot-tags-container a.tag').each((_, el) => {
            tags.push($(el).text().trim());
        });

        // Extract description
        const description = $('.shot-description').text().trim();

        // Extract structured data
        let structuredData = {};
        const jsonLd = $('script[type="application/ld+json"]').html();
        if (jsonLd) {
            try {
                structuredData = JSON.parse(jsonLd);
            } catch (e) { /* ignore parse errors */ }
        }

        return { colors, tags, description, structuredData };
    }

    async scrapeDesignerProfile(username) {
        const url = `${this.baseUrl}/${username}`;
        const { data } = await this.client.get(url);
        const $ = cheerio.load(data);

        const stats = {};
        $('.profile-stats li').each((_, el) => {
            const label = $(el).find('.stat-label').text().trim().toLowerCase();
            const value = $(el).find('.stat-value').text().trim();
            stats[label] = this.parseCount(value);
        });

        return {
            username,
            name: $('.profile-name').text().trim() || username,
            bio: $('.profile-bio').text().trim(),
            location: $('.profile-location').text().trim(),
            followers: stats.followers || 0,
            following: stats.following || 0,
            shotsCount: stats.shots || 0
        };
    }

    extractShotId(url) {
        const match = url.match(/\/shots\/(\d+)/);
        return match ? parseInt(match[1]) : null;
    }

    parseCount(text) {
        if (!text) return 0;
        text = text.toLowerCase().replace(/,/g, '');
        if (text.includes('k')) return Math.round(parseFloat(text) * 1000);
        if (text.includes('m')) return Math.round(parseFloat(text) * 1000000);
        return parseInt(text) || 0;
    }
}

// Usage
(async () => {
    const scraper = new DribbbleScraper();

    const shots = await scraper.scrapeShotsListing('popular', 1);
    console.log(`Found ${shots.length} shots`);

    for (const shot of shots.slice(0, 3)) {
        console.log(`  ${shot.title} - ${shot.likes} likes`);

        const details = await scraper.scrapeShotDetail(shot.url);
        console.log(`    Colors: ${details.colors.join(', ')}`);
        console.log(`    Tags: ${details.tags.join(', ')}`);

        // Rate limiting
        await new Promise(r => setTimeout(r, 1000));
    }
})();
Enter fullscreen mode Exit fullscreen mode

Extracting Color Palettes and Design Trends

One of the most valuable aspects of Dribbble scraping is analyzing color trends across thousands of designs. Here's how to aggregate color data:

from collections import Counter
import colorsys

def analyze_color_trends(shots_with_colors):
    """Analyze color trends across multiple shots."""
    all_colors = []

    for shot in shots_with_colors:
        all_colors.extend(shot.get("colors", []))

    # Count most common colors
    color_counter = Counter(all_colors)
    top_colors = color_counter.most_common(20)

    # Group by color family
    color_families = {
        "red": [], "orange": [], "yellow": [],
        "green": [], "blue": [], "purple": [],
        "neutral": []
    }

    for hex_color, count in color_counter.items():
        family = classify_color_family(hex_color)
        color_families[family].append((hex_color, count))

    return {
        "top_colors": top_colors,
        "color_families": {
            k: sorted(v, key=lambda x: x[1], reverse=True)[:5]
            for k, v in color_families.items()
        },
        "total_unique_colors": len(color_counter)
    }

def classify_color_family(hex_color):
    """Classify a hex color into a color family."""
    hex_color = hex_color.lstrip("#")
    if len(hex_color) != 6:
        return "neutral"

    r, g, b = int(hex_color[:2], 16), int(hex_color[2:4], 16), int(hex_color[4:], 16)
    h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)

    if s < 0.1:
        return "neutral"

    hue_deg = h * 360
    if hue_deg < 15 or hue_deg >= 345:
        return "red"
    elif hue_deg < 45:
        return "orange"
    elif hue_deg < 75:
        return "yellow"
    elif hue_deg < 165:
        return "green"
    elif hue_deg < 255:
        return "blue"
    else:
        return "purple"
Enter fullscreen mode Exit fullscreen mode

Scaling with Apify

While the scripts above work for small-scale extraction, scraping Dribbble at scale introduces challenges: rate limiting, IP blocking, JavaScript rendering, and infrastructure management. This is where Apify shines.

Apify provides cloud-based scraping infrastructure with built-in proxy rotation, browser automation, and data storage. You can find ready-made Dribbble scrapers on the Apify Store, or build your own custom actor.

Using an Apify Actor for Dribbble

from apify_client import ApifyClient

# Initialize the client
client = ApifyClient("your_apify_api_token")

# Run a Dribbble scraper actor
run_input = {
    "searchTerms": ["mobile design", "dashboard ui", "logo design"],
    "maxItems": 500,
    "includeDetails": True,
    "includeColors": True,
    "includeDesignerProfiles": True,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"]
    }
}

# Start the actor run
run = client.actor("your-dribbble-actor-id").call(run_input=run_input)

# Fetch results from the dataset
dataset_items = client.dataset(run["defaultDatasetId"]).list_items().items

for item in dataset_items:
    print(f"Shot: {item['title']}")
    print(f"  Designer: {item['designer']}")
    print(f"  Likes: {item['likes']} | Views: {item['views']}")
    print(f"  Colors: {item.get('colors', [])}")
    print(f"  Tags: {item.get('tags', [])}")
    print()
Enter fullscreen mode Exit fullscreen mode

Node.js Apify Integration

const { ApifyClient } = require('apify-client');

const client = new ApifyClient({ token: 'your_apify_api_token' });

async function scrapeDribbbleAtScale() {
    const run = await client.actor('your-dribbble-actor-id').call({
        searchTerms: ['web design', 'illustration', 'branding'],
        maxItems: 1000,
        includeDetails: true,
        includeColors: true,
        proxy: {
            useApifyProxy: true,
            apifyProxyGroups: ['RESIDENTIAL']
        }
    });

    const { items } = await client.dataset(run.defaultDatasetId).listItems();

    console.log(`Scraped ${items.length} shots`);

    // Analyze trends
    const tagCounts = {};
    items.forEach(item => {
        (item.tags || []).forEach(tag => {
            tagCounts[tag] = (tagCounts[tag] || 0) + 1;
        });
    });

    const topTags = Object.entries(tagCounts)
        .sort((a, b) => b[1] - a[1])
        .slice(0, 20);

    console.log('Top design trends:');
    topTags.forEach(([tag, count]) => {
        console.log(`  ${tag}: ${count} shots`);
    });
}

scrapeDribbbleAtScale();
Enter fullscreen mode Exit fullscreen mode

Handling Following and Social Graph Data

Understanding the social connections between designers can reveal valuable insights about design communities and influence networks:

def scrape_following_network(scraper, username, depth=1):
    """Build a following network starting from a designer."""
    visited = set()
    network = {"nodes": [], "edges": []}
    queue = [(username, 0)]

    while queue:
        current_user, current_depth = queue.pop(0)

        if current_user in visited or current_depth > depth:
            continue

        visited.add(current_user)

        # Get profile
        profile = scraper.scrape_designer_profile(current_user)
        network["nodes"].append(profile)

        # Get following list
        following = scrape_following_list(scraper, current_user)

        for followed_user in following:
            network["edges"].append({
                "from": current_user,
                "to": followed_user
            })

            if current_depth < depth:
                queue.append((followed_user, current_depth + 1))

        time.sleep(2)  # Respect rate limits

    return network
Enter fullscreen mode Exit fullscreen mode

Best Practices and Legal Considerations

When scraping Dribbble, keep these guidelines in mind:

  1. Respect robots.txt: Always check and follow Dribbble's robots.txt directives
  2. Rate limiting: Keep requests to 1-2 per second maximum to avoid overwhelming their servers
  3. Terms of Service: Review Dribbble's ToS regarding data collection and usage
  4. Attribution: If you display scraped designs, always credit the original designers
  5. Personal data: Be cautious with personal information (emails, real names) under GDPR and similar regulations
  6. Caching: Store results locally to avoid redundant requests
  7. API first: If Dribbble offers an official API for your use case, prefer that over scraping

Conclusion

Scraping Dribbble provides powerful insights into the design industry — from trending color palettes and popular design styles to designer talent discovery and competitive analysis. By combining Python or Node.js scrapers with cloud platforms like Apify, you can extract and analyze design data at scale while respecting rate limits and platform guidelines.

The key is starting with clear objectives: know what data you need, build focused scrapers for those specific data points, and use the extracted data responsibly. Whether you're tracking design trends, building a talent pipeline, or analyzing the creative landscape, Dribbble's rich visual data offers unique opportunities for data-driven insights in the design world.

Remember to always comply with Dribbble's terms of service and applicable data protection laws when implementing any scraping solution.

Top comments (0)