diff --git a/src/polymesh/impl/impl_ranges.hh b/src/polymesh/impl/impl_ranges.hh
index 4d5ecd33b93750ff98d3f095991646220cea85d8..52d0cb075392d3770a558d20bc53d8ae2dcc627d 100644
--- a/src/polymesh/impl/impl_ranges.hh
+++ b/src/polymesh/impl/impl_ranges.hh
@@ -328,6 +328,25 @@ auto smart_range<this_t, ElementT>::aabb(FuncT&& f) const -> polymesh::minmax_t<
     return r;
 }
 
+template <class this_t, class ElementT>
+template <class Generator, class FuncT>
+auto smart_range<this_t, ElementT>::random(Generator& g, FuncT&& f) const -> tmp::decayed_result_type_of<FuncT, ElementT>
+{
+    auto it = static_cast<this_t const*>(this)->begin();
+    POLYMESH_ASSERT(it.is_valid() && "requires non-empty range");
+    auto v = f(*it);
+    double cnt = 2;
+    ++it;
+    while (it.is_valid())
+    {
+        if (double(g()) * cnt < double(g.max()))
+            v = f(*it);
+        ++it;
+        cnt += 1;
+    }
+    return v;
+}
+
 template <class this_t, class ElementT>
 template <class FuncT>
 auto smart_range<this_t, ElementT>::minmax(FuncT&& f) const -> polymesh::minmax_t<tmp::decayed_result_type_of<FuncT, ElementT>>
diff --git a/src/polymesh/ranges.hh b/src/polymesh/ranges.hh
index 7ef23663285179374d0902a0645264ea784f5339..5e86a0eb7dd360ccecc4d70f54f706d79529f1b1 100644
--- a/src/polymesh/ranges.hh
+++ b/src/polymesh/ranges.hh
@@ -161,6 +161,11 @@ struct smart_range
     template <class PredT>
     auto filter(PredT&& p) -> filtered_range<ElementT, this_t&, PredT>;
 
+    /// returns a uniformly random sampled element from this range
+    /// complexity is O(n) (but does not allocate)
+    template <class Generator, class FuncT = tmp::identity>
+    auto random(Generator& g, FuncT&& f = {}) const -> tmp::decayed_result_type_of<FuncT, ElementT>;
+
     // TODO: (requires new ranges)
     // - filter (or where?)
     // - map
@@ -233,6 +238,7 @@ struct smart_collection : smart_range<smart_collection<mesh_ptr, tag, iterator>,
     end_iterator end() const { return {}; }
 
     /// returns a handle chosen uniformly at random
+    /// Complexity is usually O(1)
     /// NOTE: when only valid handles are allowed, this will use rejection sampling
     ///       and thus get very slow if a lot of data is invalid
     template <class Generator>